From b2affe547914c6a4820a08cf164637cb3024185e Mon Sep 17 00:00:00 2001 From: Marc Sauer Date: Tue, 21 Jul 2026 22:36:11 +0200 Subject: [PATCH] Fix /docs base URL showing localhost on deployed instances Switch Scribe from static-HTML to its `laravel` type so the docs page renders at request time and its base URL resolves to the live APP_URL, instead of the build-time value baked into static files. - config/scribe.php: type=laravel, base_url='{{ config("app.url") }}', add_routes=false, OpenAPI/Postman specs disabled; drop the obsolete class_exists(AuthIn) guard - composer: move knuckleswtf/scribe from require-dev to require (runtime dependency); minimal lock change (9 packages moved dev->prod, no bumps) - routes/web.php: serve /docs via Route::view('scribe.index'); remove the old /docs -> /docs/index.html redirect - Dockerfile.prod: drop the vendor-dev/docs stages, generate in the runtime stage; generate against a sentinel base URL then inject the Blade expression so example snippets and "Try It Out" host are dynamic too - nginx.conf: route /docs through PHP; simplify index directive - gitignore new generated paths; untrack the stale committed view that had localhost baked in - update dev docs compose service comment and CLAUDE.md --- .gitignore | 8 +- CLAUDE.md | 10 +- Docker/Dockerfile.prod | 75 +- Docker/docker-compose.yml | 7 +- Docker/nginx.conf | 7 +- composer.json | 2 +- composer.lock | 1452 +++++----- config/scribe.php | 40 +- resources/views/scribe/index.blade.php | 3385 ------------------------ routes/web.php | 9 +- 10 files changed, 796 insertions(+), 4199 deletions(-) delete mode 100644 resources/views/scribe/index.blade.php diff --git a/.gitignore b/.gitignore index 5c4cc74..b9d19f2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,10 @@ yarn-error.log .DS_Store # Scribe generated API docs - regenerated via `php artisan scribe:generate` -# (doc content lives in config/scribe.php + controller annotations). Output is -# static HTML built into the prod image (Docker/Dockerfile.prod `docs` stage). +# (doc content lives in config/scribe.php + controller annotations). laravel-type +# output: the Blade docs view + its assets, baked into the prod image at build time +# (Docker/Dockerfile.prod) and regenerated on every dev `up`. /.scribe -/public/docs +/resources/views/scribe +/public/vendor/scribe /storage/app/scribe diff --git a/CLAUDE.md b/CLAUDE.md index e5c383b..fdf5d48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,13 +169,13 @@ All API responses go through Laravel API Resources at `app/Http/Resources/V1/`. ### API Documentation (Scribe) -The `/api/v1` REST API is documented with [Scribe](https://scribe.knuckles.wtf) (`knuckleswtf/scribe`, a dev dependency). Docs are generated as **static HTML** into `public/docs/` and served **directly by the web server** (nginx in prod) as static files at **`/docs`** — there is no PHP `/docs` route and no `scribe` named route. An OpenAPI spec and Postman collection are emitted alongside under `public/docs/`. An "API Reference" link is in the footer of both the `app` and `landing` layouts (`url('/docs')`). +The `/api/v1` REST API is documented with [Scribe](https://scribe.knuckles.wtf) (`knuckleswtf/scribe`, a **runtime** dependency in `require`). Docs are generated as a **`laravel`-type Blade view** (`resources/views/scribe/index.blade.php` + assets under `public/vendor/scribe/`) and served by a Laravel route at **`/docs`** — `Route::view('/docs', 'scribe.index')->name('scribe')` in `routes/web.php`. Because the page is rendered at request time, its **base URL resolves to the live `APP_URL`** on each request (fixing the old static-HTML bug where the build-time `localhost` was baked in). An "API Reference" link is in the footer of both the `app` and `landing` layouts (`url('/docs')`). -- **Config** — `config/scribe.php`: `type => 'static'` (output `public/docs`), `routes.match.prefixes => ['api/*']` (only API routes are documented), bearer-token auth (`auth.default => true`, so every endpoint requires a Sanctum token except those marked `@unauthenticated` — currently only `GET /api/v1/info`). The live `ResponseCalls` strategy is removed so generation is deterministic and never touches the DB. The whole config is guarded by `if (! class_exists(AuthIn::class)) return [];` — Scribe is dev-only and absent from the `--no-dev` production runtime image, so this stops `config:cache` (run by `php artisan optimize` on every container start) from fataling on the missing `Knuckles\Scribe\*` classes. +- **Config** — `config/scribe.php`: `type => 'laravel'`; `base_url => '{{ config("app.url") }}'` (the dynamic-string form Scribe renders at request time, so the docs heading always shows this instance's URL); `laravel.add_routes => false` (routing is manual — see above — so only the `/docs` page is exposed, not the `.postman`/`.openapi` spec routes); `postman.enabled`/`openapi.enabled => false` (the machine-readable specs are dropped: they are raw static files that can't use the dynamic base URL, and their laravel-type output dir `storage/app/scribe` is hidden by the prod storage volume). `routes.match.prefixes => ['api/*']` (only API routes are documented); bearer-token auth (`auth.default => true`, so every endpoint needs a Sanctum token except those marked `@unauthenticated` — currently only `GET /api/v1/info`). The live `ResponseCalls` strategy is removed so generation is deterministic and never touches the DB. (Scribe is now always installed, so the old `if (! class_exists(AuthIn::class)) return [];` config guard is gone.) - **Doc content lives in source** — example responses, request-body fields, groups and auth flags are Scribe docblock annotations (`@group`, `@authenticated`/`@unauthenticated`, `@urlParam`, `@queryParam`, `@bodyParam`, `@response`) on the `app/Http/Controllers/Api/V1/*` controllers. `@bodyParam` values mirror the matching `Store*Request` rules. Keep them in sync when the API changes. The `GET /api/v1/user` endpoint was extracted from an inline route closure into `UserController` so it could be annotated. -- **Generation** — `php artisan scribe:generate` writes static HTML to `public/docs/`; the output plus intermediates (`.scribe/`, `storage/app/scribe/`) are **gitignored**, so it must be (re)generated wherever the app is deployed or `/docs` 404s. - - **Production** — baked into the image at build time: `Docker/Dockerfile.prod` runs `scribe:generate` in a dedicated `docs` stage (which installs the dev deps that carry Scribe) and `COPY --from=docs`es `public/docs` into the Scribe-free runtime image. nginx serves it statically (`index index.php index.html` in `Docker/nginx.conf` resolves `/docs` → `public/docs/index.html`). No deploy step needed. - - **Dev** — the one-shot `docs` service in `Docker/docker-compose.yml` runs `scribe:generate` into the mounted repo before `app` serves; `php artisan serve` serves the static files, and `routes/web.php` has a `Route::redirect('/docs', '/docs/index.html')` so `/docs` resolves (the built-in dev server won't auto-serve a directory index). Regenerate manually with `docker exec yaams-dev-app php artisan scribe:generate` after editing annotations. +- **Generation** — `php artisan scribe:generate` writes the Blade view + assets; the output plus intermediates (`resources/views/scribe/`, `public/vendor/scribe/`, `.scribe/`, `storage/app/scribe/`) are **gitignored**, so it must be (re)generated wherever the app is deployed or `/docs` 500s on the missing view. + - **Production** — baked into the image at build time: `Docker/Dockerfile.prod` runs `scribe:generate` in the runtime stage (Scribe is in the runtime `vendor`, no separate docs/dev-vendor stage). To make the copy-paste example snippets and the "Try It Out" host dynamic too (Scribe pre-renders `base_url` into those, which would otherwise freeze to the build-time URL), it generates against a sentinel base URL (`APP_URL=https://scribe-base-url.invalid`) and then `sed`-replaces that sentinel with `{{ config('app.url') }}` directly in the generated view — the request-time Blade render then resolves every occurrence to the real `APP_URL`. An arbitrary runtime UID cannot write `public/`/`resources/`, which is why generation happens at build (as root), not at container start. + - **Dev** — the one-shot `docs` service in `Docker/docker-compose.yml` runs `scribe:generate` into the mounted repo before `app` serves; `php artisan serve` then serves `/docs` via the Laravel route. Dev needs no sentinel/`sed` — it generates against the dev `APP_URL` (`http://localhost:8000`), which is exactly where the dev server runs. Regenerate manually with `docker exec yaams-dev-app php artisan scribe:generate` after editing annotations. ### Prometheus Metrics diff --git a/Docker/Dockerfile.prod b/Docker/Dockerfile.prod index cb7fd0c..85f91cf 100644 --- a/Docker/Dockerfile.prod +++ b/Docker/Dockerfile.prod @@ -36,47 +36,6 @@ RUN --mount=type=cache,target=/tmp/composer-cache \ --optimize-autoloader \ --ignore-platform-req=ext-gd -# --------------------------------------------------------------------------- -# Stage 1b - Composer dependencies WITH dev packages (only for doc generation) -# --------------------------------------------------------------------------- -# Scribe (knuckleswtf/scribe) is a dev dependency, so it is deliberately absent -# from the runtime `vendor` stage above. This throwaway stage installs it just -# so the `docs` stage below can run `artisan scribe:generate`. `--ignore-platform-reqs` -# because the composer image lacks the app's PHP extensions - we only need the files. -FROM composer:2 AS vendor-dev -WORKDIR /app -COPY composer.json composer.lock ./ -RUN --mount=type=cache,target=/tmp/composer-cache \ - COMPOSER_CACHE_DIR=/tmp/composer-cache \ - composer install \ - --no-scripts \ - --no-interaction \ - --prefer-dist \ - --ignore-platform-reqs - -# --------------------------------------------------------------------------- -# Stage 1c - Generate the static API docs (public/docs) with Scribe -# --------------------------------------------------------------------------- -# Runs `scribe:generate` (type=static) to emit public/docs/, copied into the -# runtime image below and served by nginx. App boot here is DB-less: the live -# ResponseCalls strategy is removed (config/scribe.php) and AppServiceProvider's -# only boot-time DB access is wrapped in rescue(), so no database is needed. -FROM php:8.3-cli-alpine AS docs -COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ -RUN install-php-extensions pdo_mysql intl -WORKDIR /app -COPY --from=vendor-dev /app/vendor ./vendor -COPY . . -# Throwaway env for this ephemeral build stage only (never shipped) - set inline on -# the RUN so it doesn't linger as an image ENV (and doesn't trip secret-in-ENV linters). -RUN APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \ - APP_ENV=production \ - CACHE_STORE=array \ - SESSION_DRIVER=array \ - QUEUE_CONNECTION=sync \ - MAIL_MAILER=log \ - php artisan scribe:generate - # --------------------------------------------------------------------------- # Stage 2 - Runtime (nginx + PHP-FPM on Alpine) # --------------------------------------------------------------------------- @@ -116,13 +75,39 @@ RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY Docker/nginx.conf /etc/nginx/nginx.conf COPY Docker/supervisord.conf /etc/supervisord.conf -# App source + resolved vendor from stage 1. +# App source + resolved vendor from stage 1. Scribe is now a runtime dependency +# (composer `require`), so it is present in the vendor stage. COPY . /app COPY --from=vendor /app/vendor /app/vendor -# Static API docs (Scribe), pre-generated in the `docs` stage; served by nginx. -# The runtime image itself has no Scribe (dev-only dependency). -COPY --from=docs /app/public/docs /app/public/docs +# Generate the Scribe API docs (laravel type) into the image: the Blade view at +# resources/views/scribe/ + assets under public/vendor/scribe/, served at /docs by a +# Laravel route. Runs as root so the baked-in output is readable by any runtime UID +# (an unprivileged UID could not write these paths at container start). +# +# Making the docs base URL dynamic: the docs page heading uses the base_url config +# verbatim ('{{ config("app.url") }}'), so it already resolves at request time. But +# Scribe pre-renders that string when baking the copy-paste example snippets and the +# "Try It Out" host, so those would otherwise freeze to whatever APP_URL was at build +# time (unknown here - one image serves any instance). So generate against a sentinel +# base URL, then replace it with the Blade expression directly in the view (raw, after +# Scribe's HTML-escaping) - the request-time Blade render then resolves every occurrence +# to this instance's real APP_URL. Single quotes inside config() keep it valid once +# baked into the JS/HTML without further escaping. +# +# Throwaway env for this build stage only (never shipped) - set inline on the RUN so it +# doesn't linger as an image ENV. DB-less: ResponseCalls is removed (config/scribe.php) +# and AppServiceProvider's only boot-time DB access is rescue()-wrapped. +RUN APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \ + APP_ENV=production \ + APP_URL=https://scribe-base-url.invalid \ + CACHE_STORE=array \ + SESSION_DRIVER=array \ + QUEUE_CONNECTION=sync \ + MAIL_MAILER=log \ + php artisan scribe:generate \ + && sed -i "s#https://scribe-base-url.invalid#{{ config('app.url') }}#g" \ + resources/views/scribe/index.blade.php # Storage + cache must be writable by the runtime user, and nginx needs its # lib dir writable too. Own them by the root group (GID 0) with group perms diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index 9b3e9d7..613dbce 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -80,9 +80,10 @@ services: - "yaams" restart: "no" - # One-shot: generates the static Scribe API docs into public/docs in the mounted - # repo before the app starts, so a fresh dev stack never 404s the docs page. The - # dev app (php artisan serve) serves them at /docs via the redirect in routes/web.php. + # One-shot: generates the Scribe API docs (laravel type - a Blade view at + # resources/views/scribe + assets under public/vendor/scribe) into the mounted repo + # before the app starts, so a fresh dev stack never 404s the docs page. The dev app + # (php artisan serve) serves them at /docs via the route Scribe registers. # Output is gitignored and deterministic (no DB needed), so it is safe to re-run on # every `up`. In production the docs are baked into the image instead (Dockerfile.prod). docs: diff --git a/Docker/nginx.conf b/Docker/nginx.conf index 8429859..ab7747d 100644 --- a/Docker/nginx.conf +++ b/Docker/nginx.conf @@ -39,9 +39,10 @@ http { absolute_redirect off; root /app/public; - # index.html lets the pre-generated static Scribe docs resolve: /docs -> - # /docs/ -> public/docs/index.html (assets are served by try_files below). - index index.php index.html; + # /docs is a Laravel route (Scribe laravel-type Blade view), so it falls through + # the `location /` try_files to index.php; its assets under public/vendor/scribe/ + # are served statically by try_files $uri. + index index.php; location / { try_files $uri $uri/ /index.php?$query_string; diff --git a/composer.json b/composer.json index 9630217..8fe05c9 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "php": "^8.2", "ext-gd": "*", "intervention/image": "^3.11", + "knuckleswtf/scribe": "^5.11", "laravel/fortify": "^1.37", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", @@ -19,7 +20,6 @@ }, "require-dev": { "fakerphp/faker": "^1.23", - "knuckleswtf/scribe": "^5.11", "laravel/pail": "^1.2", "laravel/pint": "^1.18", "laravel/sail": "^1.29", diff --git a/composer.lock b/composer.lock index 796c1ef..2ae1090 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "11d518d23cce1125b206a18eb72c50b7", + "content-hash": "40774426477ec5af093ca3f7e75e25e8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -661,6 +661,140 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.4.0", @@ -1404,6 +1538,98 @@ ], "time": "2026-05-01T08:20:10+00:00" }, + { + "name": "knuckleswtf/scribe", + "version": "5.11.0", + "source": { + "type": "git", + "url": "https://github.com/knuckleswtf/scribe.git", + "reference": "71c8c4acac495401ab8af1a68a46f09d45aff426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/71c8c4acac495401ab8af1a68a46f09d45aff426", + "reference": "71c8c4acac495401ab8af1a68a46f09d45aff426", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-pdo": "*", + "fakerphp/faker": "^1.23.1", + "laravel/framework": "^9.21 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "league/flysystem": "^3.0", + "mpociot/reflection-docblock": "^1.0.1", + "nikic/php-parser": "^5.0", + "nunomaduro/collision": "^6.0 || ^7.0 || ^8.0", + "parsedown/parsedown": "^1.7", + "php": ">=8.1", + "ramsey/uuid": "^4.2.2", + "shalvah/upgrader": "^0.6.0", + "symfony/var-exporter": "^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^6.0 || ^7.0 || ^8.0" + }, + "replace": { + "mpociot/laravel-apidoc-generator": "*" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "laravel/legacy-factories": "^1.3.0", + "laravel/pint": "^1.20", + "league/fractal": "^0.20", + "nikic/fast-route": "^1.3", + "orchestra/testbench": "^7.0 || ^8.0 || ^9.10 || ^10.0 || ^11.0", + "pestphp/pest": "^1.21 || ^2.0 || ^3.0 || ^4.0", + "phpstan/phpstan": "^2.1.5", + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0 || ^12.0", + "spatie/ray": "^1.41", + "symfony/css-selector": "^6.0 || ^7.0 || ^8.0", + "symfony/dom-crawler": "^6.0 || ^7.0 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Knuckles\\Scribe\\ScribeServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Config/helpers.php" + ], + "psr-4": { + "Knuckles\\Camel\\": "camel/", + "Knuckles\\Scribe\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Shalvah" + } + ], + "description": "Generate API documentation for humans from your Laravel codebase.✍", + "homepage": "https://github.com/knuckleswtf/scribe", + "keywords": [ + "api", + "documentation", + "laravel" + ], + "support": { + "issues": "https://github.com/knuckleswtf/scribe/issues", + "source": "https://github.com/knuckleswtf/scribe/tree/5.11.0" + }, + "funding": [ + { + "url": "https://patreon.com/shalvah", + "type": "patreon" + } + ], + "time": "2026-06-08T15:39:10+00:00" + }, { "name": "laravel/fortify", "version": "v1.37.2", @@ -2734,39 +2960,92 @@ "time": "2026-01-02T08:56:05+00:00" }, { - "name": "nesbot/carbon", - "version": "3.13.0", + "name": "mpociot/reflection-docblock", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + "url": "https://github.com/mpociot/reflection-docblock.git", + "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "url": "https://api.github.com/repos/mpociot/reflection-docblock/zipball/c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", + "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", "shasum": "" }, "require": { - "carbonphp/carbon-doctrine-types": "<100.0", - "ext-json": "*", - "php": "^8.1", - "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", - "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" - }, - "provide": { - "psr/clock-implementation": "1.0" + "php": ">=5.3.3" }, "require-dev": { - "doctrine/dbal": "^3.6.3 || ^4.0", - "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^v3.87.1", - "kylekatarnls/multi-tester": "^2.5.3", - "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mpociot": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/mpociot/reflection-docblock/issues", + "source": "https://github.com/mpociot/reflection-docblock/tree/master" + }, + "time": "2016-06-20T20:53:12+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.13.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, @@ -3054,6 +3333,102 @@ }, "time": "2025-12-06T11:56:16+00:00" }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, { "name": "nunomaduro/termwind", "version": "v2.4.0", @@ -3210,6 +3585,56 @@ }, "time": "2025-09-24T15:06:41+00:00" }, + { + "name": "parsedown/parsedown", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/parsedown/parsedown.git", + "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/parsedown/parsedown/zipball/96baaad00f71ba04d76e45b4620f54d3beabd6f7", + "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5|^8.5|^9.6" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/parsedown/parsedown/issues", + "source": "https://github.com/parsedown/parsedown/tree/1.8.0" + }, + "time": "2026-02-16T11:41:01+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -4320,45 +4745,34 @@ "time": "2026-06-18T03:57:49+00:00" }, { - "name": "spatie/laravel-activitylog", - "version": "4.12.3", + "name": "shalvah/upgrader", + "version": "0.6.0", "source": { "type": "git", - "url": "https://github.com/spatie/laravel-activitylog.git", - "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0" + "url": "https://github.com/shalvah/upgrader.git", + "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", - "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", + "url": "https://api.github.com/repos/shalvah/upgrader/zipball/d95ed17fe9f5e1ee7d47ad835595f1af080a867f", + "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f", "shasum": "" }, "require": { - "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", - "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0", - "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", - "php": "^8.1", - "spatie/laravel-package-tools": "^1.6.3" + "illuminate/support": ">=8.0", + "nikic/php-parser": "^5.0", + "php": ">=8.0" }, "require-dev": { - "ext-json": "*", - "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0", - "pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0" + "dms/phpunit-arraysubset-asserts": "^0.2.0", + "pestphp/pest": "^1.21", + "phpstan/phpstan": "^1.0", + "spatie/ray": "^1.33" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\Activitylog\\ActivitylogServiceProvider" - ] - } - }, "autoload": { - "files": [ - "src/helpers.php" - ], "psr-4": { - "Spatie\\Activitylog\\": "src" + "Shalvah\\Upgrader\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4367,8 +4781,77 @@ ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", + "name": "Shalvah", + "email": "hello@shalvah.me" + } + ], + "description": "Create automatic upgrades for your package.", + "homepage": "http://github.com/shalvah/upgrader", + "keywords": [ + "upgrade" + ], + "support": { + "issues": "https://github.com/shalvah/upgrader/issues", + "source": "https://github.com/shalvah/upgrader/tree/0.6.0" + }, + "funding": [ + { + "url": "https://patreon.com/shalvah", + "type": "patreon" + } + ], + "time": "2024-02-20T11:51:46+00:00" + }, + { + "name": "spatie/laravel-activitylog", + "version": "4.12.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", + "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", "homepage": "https://spatie.be", "role": "Developer" }, @@ -7683,6 +8166,163 @@ ], "time": "2026-03-30T13:44:50+00:00" }, + { + "name": "symfony/var-exporter", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0118811b1d59f323bf131250b3fb919febfece28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0118811b1d59f323bf131250b3fb919febfece28", + "reference": "0118811b1d59f323bf131250b3fb919febfece28", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T08:41:53+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T06:06:12+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", @@ -8122,160 +8762,26 @@ ], "packages-dev": [ { - "name": "fakerphp/faker", - "version": "v1.24.1", + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "php": "^7.4|^8.0" }, - "conflict": { - "fzaninotto/faker": "*" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." - }, - "type": "library", - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" - }, - "time": "2024-11-21T13:46:39+00:00" - }, - { - "name": "filp/whoops", - "version": "2.18.4", - "source": { - "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^4.0 || ^5.0" - }, - "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Whoops\\": "src/Whoops/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" - } - ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", - "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" - ], - "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.4" - }, - "funding": [ - { - "url": "https://github.com/denis-sokolov", - "type": "github" - } - ], - "time": "2025-08-08T12:00:00+00:00" - }, - { - "name": "hamcrest/hamcrest-php", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "shasum": "" - }, - "require": { - "php": "^7.4|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" }, "require-dev": { "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", @@ -8306,98 +8812,6 @@ }, "time": "2025-04-30T06:54:44+00:00" }, - { - "name": "knuckleswtf/scribe", - "version": "5.11.0", - "source": { - "type": "git", - "url": "https://github.com/knuckleswtf/scribe.git", - "reference": "71c8c4acac495401ab8af1a68a46f09d45aff426" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/71c8c4acac495401ab8af1a68a46f09d45aff426", - "reference": "71c8c4acac495401ab8af1a68a46f09d45aff426", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "ext-pdo": "*", - "fakerphp/faker": "^1.23.1", - "laravel/framework": "^9.21 || ^10.0 || ^11.0 || ^12.0 || ^13.0", - "league/flysystem": "^3.0", - "mpociot/reflection-docblock": "^1.0.1", - "nikic/php-parser": "^5.0", - "nunomaduro/collision": "^6.0 || ^7.0 || ^8.0", - "parsedown/parsedown": "^1.7", - "php": ">=8.1", - "ramsey/uuid": "^4.2.2", - "shalvah/upgrader": "^0.6.0", - "symfony/var-exporter": "^6.0 || ^7.0 || ^8.0", - "symfony/yaml": "^6.0 || ^7.0 || ^8.0" - }, - "replace": { - "mpociot/laravel-apidoc-generator": "*" - }, - "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.5.0", - "laravel/legacy-factories": "^1.3.0", - "laravel/pint": "^1.20", - "league/fractal": "^0.20", - "nikic/fast-route": "^1.3", - "orchestra/testbench": "^7.0 || ^8.0 || ^9.10 || ^10.0 || ^11.0", - "pestphp/pest": "^1.21 || ^2.0 || ^3.0 || ^4.0", - "phpstan/phpstan": "^2.1.5", - "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0 || ^12.0", - "spatie/ray": "^1.41", - "symfony/css-selector": "^6.0 || ^7.0 || ^8.0", - "symfony/dom-crawler": "^6.0 || ^7.0 || ^8.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Knuckles\\Scribe\\ScribeServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/Config/helpers.php" - ], - "psr-4": { - "Knuckles\\Camel\\": "camel/", - "Knuckles\\Scribe\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Shalvah" - } - ], - "description": "Generate API documentation for humans from your Laravel codebase.✍", - "homepage": "https://github.com/knuckleswtf/scribe", - "keywords": [ - "api", - "documentation", - "laravel" - ], - "support": { - "issues": "https://github.com/knuckleswtf/scribe/issues", - "source": "https://github.com/knuckleswtf/scribe/tree/5.11.0" - }, - "funding": [ - { - "url": "https://patreon.com/shalvah", - "type": "patreon" - } - ], - "time": "2026-06-08T15:39:10+00:00" - }, { "name": "laravel/pail", "version": "v1.2.7", @@ -8692,59 +9106,6 @@ }, "time": "2024-05-16T03:13:13+00:00" }, - { - "name": "mpociot/reflection-docblock", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/mpociot/reflection-docblock.git", - "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mpociot/reflection-docblock/zipball/c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", - "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Mpociot": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "support": { - "issues": "https://github.com/mpociot/reflection-docblock/issues", - "source": "https://github.com/mpociot/reflection-docblock/tree/master" - }, - "time": "2016-06-20T20:53:12+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -8767,189 +9128,43 @@ "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2025-08-01T08:46:24+00:00" - }, - { - "name": "nunomaduro/collision", - "version": "v8.9.4", - "source": { - "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5", - "shasum": "" - }, - "require": { - "filp/whoops": "^2.18.4", - "nunomaduro/termwind": "^2.4.0", - "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.8" - }, - "conflict": { - "laravel/framework": "<11.48.0 || >=14.0.0", - "phpunit/phpunit": "<11.5.50 || >=14.0.0" - }, - "require-dev": { - "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.6", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", - "laravel/pint": "^1.29.1", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", - "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - }, - "branch-alias": { - "dev-8.x": "8.x-dev" - } - }, - "autoload": { - "files": [ - "./src/Adapters/Phpunit/Autoload.php" - ], - "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "Cli error handling for console/command-line PHP applications.", - "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "dev", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" - ], - "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" - }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2026-04-21T14:04:20+00:00" - }, - { - "name": "parsedown/parsedown", - "version": "1.8.0", - "source": { - "type": "git", - "url": "https://github.com/parsedown/parsedown.git", - "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/parsedown/parsedown/zipball/96baaad00f71ba04d76e45b4620f54d3beabd6f7", - "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5|^8.5|^9.6" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-0": { - "Parsedown": "" + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "markdown", - "parser" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "issues": "https://github.com/parsedown/parsedown/issues", - "source": "https://github.com/parsedown/parsedown/tree/1.8.0" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, - "time": "2026-02-16T11:41:01+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" }, { "name": "phar-io/manifest", @@ -10512,64 +10727,6 @@ ], "time": "2024-10-09T05:16:32+00:00" }, - { - "name": "shalvah/upgrader", - "version": "0.6.0", - "source": { - "type": "git", - "url": "https://github.com/shalvah/upgrader.git", - "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/shalvah/upgrader/zipball/d95ed17fe9f5e1ee7d47ad835595f1af080a867f", - "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f", - "shasum": "" - }, - "require": { - "illuminate/support": ">=8.0", - "nikic/php-parser": "^5.0", - "php": ">=8.0" - }, - "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.2.0", - "pestphp/pest": "^1.21", - "phpstan/phpstan": "^1.0", - "spatie/ray": "^1.33" - }, - "type": "library", - "autoload": { - "psr-4": { - "Shalvah\\Upgrader\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Shalvah", - "email": "hello@shalvah.me" - } - ], - "description": "Create automatic upgrades for your package.", - "homepage": "http://github.com/shalvah/upgrader", - "keywords": [ - "upgrade" - ], - "support": { - "issues": "https://github.com/shalvah/upgrader/issues", - "source": "https://github.com/shalvah/upgrader/tree/0.6.0" - }, - "funding": [ - { - "url": "https://patreon.com/shalvah", - "type": "patreon" - } - ], - "time": "2024-02-20T11:51:46+00:00" - }, { "name": "staabm/side-effects-detector", "version": "1.0.5", @@ -10622,163 +10779,6 @@ ], "time": "2024-10-20T05:08:20+00:00" }, - { - "name": "symfony/var-exporter", - "version": "v7.4.14", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "0118811b1d59f323bf131250b3fb919febfece28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0118811b1d59f323bf131250b3fb919febfece28", - "reference": "0118811b1d59f323bf131250b3fb919febfece28", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\VarExporter\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", - "homepage": "https://symfony.com", - "keywords": [ - "clone", - "construct", - "export", - "hydrate", - "instantiate", - "lazy-loading", - "proxy", - "serialize" - ], - "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.14" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-27T08:41:53+00:00" - }, - { - "name": "symfony/yaml", - "version": "v7.4.13", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.13" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-25T06:06:12+00:00" - }, { "name": "theseer/tokenizer", "version": "1.3.1", diff --git a/config/scribe.php b/config/scribe.php index 70350fb..b2adac7 100644 --- a/config/scribe.php +++ b/config/scribe.php @@ -9,16 +9,6 @@ // Only the most common configs are shown. See the https://scribe.knuckles.wtf/laravel/reference/config for all. -// Scribe is a dev-only dependency, absent from the production runtime image. The API -// docs are pre-generated as static HTML at build time (see Docker/Dockerfile.prod) and -// served directly by nginx, so the app never needs this config at runtime. Return an -// empty config when Scribe is not installed so config:cache (php artisan optimize, run -// on every container start) doesn't fatal on the missing Knuckles\Scribe\* classes. -// The `use` imports above never autoload, so they are safe even when the classes are gone. -if (! class_exists(AuthIn::class)) { - return []; -} - return [ // The HTML for the generated documentation. 'title' => config('app.name').' API Documentation', @@ -38,7 +28,9 @@ // The base URL displayed in the docs. // If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL. - 'base_url' => config('app.url'), + // We use the dynamic form so the docs (a Blade view rendered at request time) show this + // instance's real APP_URL instead of a value baked in at build time (see Docker/Dockerfile.prod). + 'base_url' => '{{ config("app.url") }}', // Routes to include in the docs 'routes' => [ @@ -67,7 +59,7 @@ // - "static" will generate a static HTMl page in the /public/docs folder, // - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication. // - "external_static" and "external_laravel" do the same as above, but pass the OpenAPI spec as a URL to an external UI template - 'type' => 'static', + 'type' => 'laravel', // See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options 'theme' => 'default', @@ -79,8 +71,11 @@ ], 'laravel' => [ - // Whether to automatically create a docs route for you to view your generated docs. You can still set up routing manually. - 'add_routes' => true, + // Routing is set up manually (Route::view('/docs', 'scribe.index') in routes/web.php). + // `add_routes` would also register `/docs.postman` and `/docs.openapi`, which we don't + // want since those specs are disabled (see the `postman`/`openapi` sections below) and + // their routes would otherwise be dead (missing files -> null body / 404). + 'add_routes' => false, // URL path to use for the docs endpoint (if `add_routes` is true). // By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec. @@ -151,24 +146,21 @@ 'php', ], - // Generate a Postman collection (v2.1.0) in addition to HTML docs. - // For 'static' docs, the collection will be generated to public/docs/collection.json. - // For 'laravel' docs, it will be generated to storage/app/scribe/collection.json. - // Setting `laravel.add_routes` to true (above) will also add a route for the collection. + // Postman collection + OpenAPI spec are disabled: they are served as raw static files, + // so the dynamic `{{ config("app.url") }}` base_url (which only Blade renders) cannot apply + // to them, and their 'laravel'-type output dir (storage/app/scribe) is hidden by the prod + // storage volume mount. The HTML docs page is the single documentation deliverable. 'postman' => [ - 'enabled' => true, + 'enabled' => false, 'overrides' => [ // 'info.version' => '2.0.0', ], ], - // Generate an OpenAPI spec in addition to docs webpage. - // For 'static' docs, the collection will be generated to public/docs/openapi.yaml. - // For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml. - // Setting `laravel.add_routes` to true (above) will also add a route for the spec. + // Disabled - see the note on `postman` above. 'openapi' => [ - 'enabled' => true, + 'enabled' => false, // The OpenAPI spec version to generate. Supported versions: '3.0.3', '3.1.0'. // OpenAPI 3.1 is more compatible with JSON Schema and is becoming the dominant version. diff --git a/resources/views/scribe/index.blade.php b/resources/views/scribe/index.blade.php deleted file mode 100644 index 107b597..0000000 --- a/resources/views/scribe/index.blade.php +++ /dev/null @@ -1,3385 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <title>YAAMS API Documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - MENU - navbar-image - - -
- -
- - - -
- - - -
- - - - - - - -
- - - - -
- -
-
-
-

Introduction

-

The YAAMS REST API (v1) for virtual airline management - airlines, fleet and PIREPs. All endpoints live under /api/v1 and return JSON.

- -
This documentation covers the YAAMS REST API v1. Every endpoint is served under `/api/v1` and returns JSON.
-
-Except for the public `GET /api/v1/info` endpoint, requests must be authenticated with a personal Sanctum bearer token (see the Authenticating section below).
-
-<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
-You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
- -

Authenticating requests

-

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_AUTH_KEY}".

-

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

-

Create a personal API token from your account settings (Settings → API tokens) in the web UI, then send it as a bearer token: Authorization: Bearer {YOUR_TOKEN}. Tokens are managed with Laravel Sanctum.

- -

Account

- -

The token owner's own account.

- -

Current user

- -

-requires authentication -

- -

"Who am I" - returns the account the API token belongs to, with its -airline memberships. Useful as a token sanity check for API clients.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/user" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/user"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/user';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "id": 1,
-        "name": "Homer Simpson",
-        "email": "homer@test.com",
-        "airlines": [
-            {
-                "id": 1,
-                "name": "Example Virtual Airlines",
-                "prefix": "EV",
-                "icaoCallsign": "EVA",
-                "atcCallsign": "EXAMPLE",
-                "unitIsLbs": false,
-                "requirePirepReview": true,
-                "locationContinuity": false,
-                "createdAt": "2026-01-01T00:00:00.000000Z",
-                "updatedAt": "2026-01-01T00:00:00.000000Z"
-            }
-        ]
-    }
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/user -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-
- -

Aircraft

- -

An airline's fleet. Aircraft are scoped to their airline - an aircraft that -does not belong to {airline} resolves as a 404.

- -

List fleet

- -

-requires authentication -

- -

List the airline's aircraft.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines/16/aircraft" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/aircraft"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/aircraft';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": [
-        {
-            "id": 7,
-            "registration": "D-EXAM",
-            "manufacturer": "Airbus",
-            "model": "A320-200",
-            "currentLoc": "EDDF",
-            "engineType": "CFM56",
-            "satcom": false,
-            "winglets": true,
-            "selcal": "AB-CD",
-            "hexCode": "3C6444",
-            "msn": "1234",
-            "mtow": 78000,
-            "mzfw": 62500,
-            "mlw": 66000,
-            "remarks": null,
-            "status": "active",
-            "active": true,
-            "retiredAt": null,
-            "retiredReason": null,
-            "inServiceSince": "2020-01-01",
-            "firstFlight": "2019-11-15",
-            "createdAt": "2026-01-01T00:00:00.000000Z",
-            "updatedAt": "2026-01-01T00:00:00.000000Z"
-        }
-    ]
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "You are not a member of this airline."
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines/{airline_id}/aircraft -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-
- -

Add an aircraft

- -

-requires authentication -

- -

Add an aircraft to the airline's fleet. Requires the "add aircraft" -permission and membership of the airline. Permission, membership, -normalization and the duplicate-registration rule are enforced by -StoreAircraftRequest.

- - -
Example request:
- - -
-
curl --request POST \
-    "http://localhost:8000/api/v1/airlines/16/aircraft" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json" \
-    --data "{
-    \"registration\": \"D-EXAM\",
-    \"manufacturer\": \"Airbus\",
-    \"model\": \"A320-200\",
-    \"engine_type\": \"CFM56\",
-    \"satcom\": false,
-    \"winglets\": true,
-    \"selcal\": \"AB-CD\",
-    \"hex_code\": \"3C6444\",
-    \"msn\": \"1234\",
-    \"mtow\": 78000,
-    \"mzfw\": 62500,
-    \"mlw\": 66000,
-    \"remarks\": \"Delivered new.\",
-    \"current_loc\": \"EDDF\"
-}"
-
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/aircraft"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-let body = {
-    "registration": "D-EXAM",
-    "manufacturer": "Airbus",
-    "model": "A320-200",
-    "engine_type": "CFM56",
-    "satcom": false,
-    "winglets": true,
-    "selcal": "AB-CD",
-    "hex_code": "3C6444",
-    "msn": "1234",
-    "mtow": 78000,
-    "mzfw": 62500,
-    "mlw": 66000,
-    "remarks": "Delivered new.",
-    "current_loc": "EDDF"
-};
-
-fetch(url, {
-    method: "POST",
-    headers,
-    body: JSON.stringify(body),
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/aircraft';
-$response = $client->post(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'json' => [
-            'registration' => 'D-EXAM',
-            'manufacturer' => 'Airbus',
-            'model' => 'A320-200',
-            'engine_type' => 'CFM56',
-            'satcom' => false,
-            'winglets' => true,
-            'selcal' => 'AB-CD',
-            'hex_code' => '3C6444',
-            'msn' => '1234',
-            'mtow' => 78000,
-            'mzfw' => 62500,
-            'mlw' => 66000,
-            'remarks' => 'Delivered new.',
-            'current_loc' => 'EDDF',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (201):

-
-
-
-{
-    "data": {
-        "id": 7,
-        "registration": "D-EXAM",
-        "manufacturer": "Airbus",
-        "model": "A320-200",
-        "currentLoc": "EDDF",
-        "engineType": "CFM56",
-        "satcom": false,
-        "winglets": true,
-        "selcal": "AB-CD",
-        "hexCode": "3C6444",
-        "msn": "1234",
-        "mtow": 78000,
-        "mzfw": 62500,
-        "mlw": 66000,
-        "remarks": null,
-        "status": "active",
-        "active": true,
-        "retiredAt": null,
-        "retiredReason": null,
-        "inServiceSince": null,
-        "firstFlight": null,
-        "createdAt": "2026-07-11T12:00:00.000000Z",
-        "updatedAt": "2026-07-11T12:00:00.000000Z"
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "This action is unauthorized."
-}
- 
-
-

Example response (422):

-
-
-
-{
-    "message": "The registration field is required.",
-    "errors": {
-        "registration": [
-            "The registration field is required."
-        ]
-    }
-}
- 
-
- - -
-

- Request    - -    - -

-

- POST - api/v1/airlines/{airline_id}/aircraft -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-

Body Parameters

-
- registration   -string  -   -   - -
-

Tail number, e.g. D-EXAM (max 9 chars, uppercased). Example: D-EXAM

-
-
- manufacturer   -string  -   -   - -
-

Airframe manufacturer, max 100 chars. Example: Airbus

-
-
- model   -string  -   -   - -
-

Aircraft model, max 100 chars. Example: A320-200

-
-
- engine_type   -string  -optional   -   - -
-

Engine type, max 100 chars. Example: CFM56

-
-
- satcom   -boolean  -optional   -   - - -
-

Whether the aircraft has SATCOM. Example: false

-
-
- winglets   -boolean  -optional   -   - - -
-

Whether the aircraft has winglets. Example: true

-
-
- selcal   -string  -optional   -   - -
-

SELCAL code, format XX-XX. Example: AB-CD

-
-
- hex_code   -string  -optional   -   - -
-

Mode-S hex code, 6 hex chars. Example: 3C6444

-
-
- msn   -string  -optional   -   - -
-

Manufacturer serial number, 1-6 digits. Example: 1234

-
-
- mtow   -integer  -optional   -   - -
-

Max take-off weight (kg), 0-1000000. Example: 78000

-
-
- mzfw   -integer  -optional   -   - -
-

Max zero-fuel weight (kg), 0-1000000. Example: 62500

-
-
- mlw   -integer  -optional   -   - -
-

Max landing weight (kg), 0-1000000. Example: 66000

-
-
- remarks   -string  -optional   -   - -
-

Free-text remarks, max 1000 chars. Example: Delivered new.

-
-
- current_loc   -string  -   -   - -
-

ICAO code of the aircraft's current location (must exist in airports). Example: EDDF

-
-
- -

Show an aircraft

- -

-requires authentication -

- -

The scoped route binding 404s any aircraft that does not belong to {airline}.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines/16/aircraft/16" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/aircraft/16"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/aircraft/16';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "id": 7,
-        "registration": "D-EXAM",
-        "manufacturer": "Airbus",
-        "model": "A320-200",
-        "currentLoc": "EDDF",
-        "engineType": "CFM56",
-        "satcom": false,
-        "winglets": true,
-        "selcal": "AB-CD",
-        "hexCode": "3C6444",
-        "msn": "1234",
-        "mtow": 78000,
-        "mzfw": 62500,
-        "mlw": 66000,
-        "remarks": null,
-        "status": "active",
-        "active": true,
-        "retiredAt": null,
-        "retiredReason": null,
-        "inServiceSince": "2020-01-01",
-        "firstFlight": "2019-11-15",
-        "createdAt": "2026-01-01T00:00:00.000000Z",
-        "updatedAt": "2026-01-01T00:00:00.000000Z"
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "You are not a member of this airline."
-}
- 
-
-

Example response (404):

-
-
-
-{
-    "message": "No query results for model [App\\Models\\Aircraft] 999"
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines/{airline_id}/aircraft/{id} -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- id   -integer  -   -   - -
-

The ID of the aircraft. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-
- aircraft   -integer  -   -   - -
-

The aircraft ID (must belong to the airline). Example: 7

-
-
- -

Airlines

- -

Virtual airlines. Aircraft and flights are nested resources of an airline.

- -

List airlines

- -

-requires authentication -

- - - - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines?members_only=1" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines"
-);
-
-const params = {
-    "members_only": "1",
-};
-Object.keys(params)
-    .forEach(key => url.searchParams.append(key, params[key]));
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'query' => [
-            'members_only' => '1',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": [
-        {
-            "id": 1,
-            "name": "Example Virtual Airlines",
-            "prefix": "EV",
-            "icaoCallsign": "EVA",
-            "atcCallsign": "EXAMPLE",
-            "unitIsLbs": false,
-            "requirePirepReview": true,
-            "locationContinuity": false,
-            "createdAt": "2026-01-01T00:00:00.000000Z",
-            "updatedAt": "2026-01-01T00:00:00.000000Z"
-        }
-    ]
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

Query Parameters

-
- members_only   -boolean  -optional   -   - - -
-

Only return airlines the token owner belongs to. Defaults to all airlines. Example: true

-
-
- -

Found a new airline

- -

-requires authentication -

- -

Requires the "add airlines" permission (enforced by StoreAirlineRequest).

- - -
Example request:
- - -
-
curl --request POST \
-    "http://localhost:8000/api/v1/airlines" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json" \
-    --data "{
-    \"name\": \"Example Virtual Airlines\",
-    \"prefix\": \"EV\",
-    \"icao_callsign\": \"EVA\",
-    \"atc_callsign\": \"EXAMPLE\"
-}"
-
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-let body = {
-    "name": "Example Virtual Airlines",
-    "prefix": "EV",
-    "icao_callsign": "EVA",
-    "atc_callsign": "EXAMPLE"
-};
-
-fetch(url, {
-    method: "POST",
-    headers,
-    body: JSON.stringify(body),
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines';
-$response = $client->post(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'json' => [
-            'name' => 'Example Virtual Airlines',
-            'prefix' => 'EV',
-            'icao_callsign' => 'EVA',
-            'atc_callsign' => 'EXAMPLE',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (201):

-
-
-
-{
-    "data": {
-        "id": 2,
-        "name": "Example Virtual Airlines",
-        "prefix": "EV",
-        "icaoCallsign": "EVA",
-        "atcCallsign": "EXAMPLE",
-        "unitIsLbs": false,
-        "requirePirepReview": true,
-        "locationContinuity": false,
-        "createdAt": "2026-01-01T00:00:00.000000Z",
-        "updatedAt": "2026-01-01T00:00:00.000000Z"
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "This action is unauthorized."
-}
- 
-
-

Example response (422):

-
-
-
-{
-    "message": "The prefix has already been taken.",
-    "errors": {
-        "prefix": [
-            "The prefix has already been taken."
-        ]
-    }
-}
- 
-
- - -
-

- Request    - -    - -

-

- POST - api/v1/airlines -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

Body Parameters

-
- name   -string  -   -   - -
-

Display name, max 50 chars, unique. Example: Example Virtual Airlines

-
-
- prefix   -string  -   -   - -
-

Two-letter IATA-style prefix, unique, uppercased. Example: EV

-
-
- icao_callsign   -string  -   -   - -
-

Three-letter ICAO callsign, unique, uppercased. Example: EVA

-
-
- atc_callsign   -string  -   -   - -
-

Spoken ATC callsign, max 25 chars, unique. Example: EXAMPLE

-
-
- -

Show an airline

- -

-requires authentication -

- - - - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines/16" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "id": 1,
-        "name": "Example Virtual Airlines",
-        "prefix": "EV",
-        "icaoCallsign": "EVA",
-        "atcCallsign": "EXAMPLE",
-        "unitIsLbs": false,
-        "requirePirepReview": true,
-        "locationContinuity": false,
-        "createdAt": "2026-01-01T00:00:00.000000Z",
-        "updatedAt": "2026-01-01T00:00:00.000000Z"
-    }
-}
- 
-
-

Example response (404):

-
-
-
-{
-    "message": "No query results for model [App\\Models\\Airline] 999"
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines/{id} -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-
- -

Flights

- -

PIREPs (pilot reports / flight logs) for an airline. Filing, listing, and - -for reviewers - the review queue plus accept/reject.

- -

List my flights

- -

-requires authentication -

- -

List the authenticated pilot's own flights for a specific airline.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines/16/flights?status_id=2" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/flights"
-);
-
-const params = {
-    "status_id": "2",
-};
-Object.keys(params)
-    .forEach(key => url.searchParams.append(key, params[key]));
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/flights';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'query' => [
-            'status_id' => '2',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": [
-        {
-            "id": 42,
-            "callsign": "421",
-            "flightNumber": "421",
-            "fullFlightNumber": "EV421",
-            "fullIcaoCallsign": "EVA421",
-            "departureIcao": "EDDF",
-            "arrivalIcao": "EGLL",
-            "cruiseAltitude": 36000,
-            "blockOff": "2026-07-11 10:00:00",
-            "blockOn": "2026-07-11 11:30:00",
-            "duration": "01:30",
-            "burnedFuel": 4200,
-            "route": "SOVAT UL610 KONAN",
-            "onlineNetwork": 1,
-            "status": {
-                "id": 2,
-                "name": "Accepted"
-            },
-            "remarks": null,
-            "rejectionRemarks": null,
-            "createdAt": "2026-07-11T11:35:00.000000Z",
-            "updatedAt": "2026-07-11T11:40:00.000000Z"
-        }
-    ]
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "You are not a member of this airline."
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines/{airline_id}/flights -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-

Query Parameters

-
- status_id   -integer  -optional   -   - -
-

Filter by flight status: 1 = Pending, 2 = Accepted, 3 = Rejected. Example: 2

-
-
- -

File a PIREP

- -

-requires authentication -

- -

Submit a new PIREP for the airline. Membership, airport/aircraft -existence and the location-continuity rule are enforced by -StoreFlightRequest. If the airline requires review the flight is created -Pending (status 1) and reviewers are notified; otherwise it is Accepted -(status 2) immediately.

- - -
Example request:
- - -
-
curl --request POST \
-    "http://localhost:8000/api/v1/airlines/16/flights" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json" \
-    --data "{
-    \"flightnumber\": 421,
-    \"departure_icao\": \"EDDF\",
-    \"arrival_icao\": \"EGLL\",
-    \"aircraft_id\": 7,
-    \"callsign\": \"421\",
-    \"crzalt\": 36000,
-    \"blockoff\": \"2026-07-11 10:00:00\",
-    \"blockon\": \"2026-07-11 11:30:00\",
-    \"burned_fuel\": 4200,
-    \"route\": \"SOVAT UL610 KONAN\",
-    \"online_network_id\": 1,
-    \"remarks\": \"Smooth flight.\"
-}"
-
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/flights"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-let body = {
-    "flightnumber": 421,
-    "departure_icao": "EDDF",
-    "arrival_icao": "EGLL",
-    "aircraft_id": 7,
-    "callsign": "421",
-    "crzalt": 36000,
-    "blockoff": "2026-07-11 10:00:00",
-    "blockon": "2026-07-11 11:30:00",
-    "burned_fuel": 4200,
-    "route": "SOVAT UL610 KONAN",
-    "online_network_id": 1,
-    "remarks": "Smooth flight."
-};
-
-fetch(url, {
-    method: "POST",
-    headers,
-    body: JSON.stringify(body),
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/flights';
-$response = $client->post(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'json' => [
-            'flightnumber' => 421,
-            'departure_icao' => 'EDDF',
-            'arrival_icao' => 'EGLL',
-            'aircraft_id' => 7,
-            'callsign' => '421',
-            'crzalt' => 36000,
-            'blockoff' => '2026-07-11 10:00:00',
-            'blockon' => '2026-07-11 11:30:00',
-            'burned_fuel' => 4200.0,
-            'route' => 'SOVAT UL610 KONAN',
-            'online_network_id' => 1,
-            'remarks' => 'Smooth flight.',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (201):

-
-
-
-{
-    "data": {
-        "id": 42,
-        "callsign": "421",
-        "flightNumber": "421",
-        "fullFlightNumber": "EV421",
-        "fullIcaoCallsign": "EVA421",
-        "departureIcao": "EDDF",
-        "arrivalIcao": "EGLL",
-        "cruiseAltitude": 36000,
-        "blockOff": "2026-07-11 10:00:00",
-        "blockOn": "2026-07-11 11:30:00",
-        "duration": "01:30",
-        "burnedFuel": 4200,
-        "route": "SOVAT UL610 KONAN",
-        "onlineNetwork": 1,
-        "status": {
-            "id": 1,
-            "name": "Pending"
-        },
-        "remarks": null,
-        "rejectionRemarks": null,
-        "createdAt": "2026-07-11T11:35:00.000000Z",
-        "updatedAt": "2026-07-11T11:35:00.000000Z"
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "This action is unauthorized."
-}
- 
-
-

Example response (422):

-
-
-
-{
-    "message": "The departure icao field is required.",
-    "errors": {
-        "departure_icao": [
-            "The departure icao field is required."
-        ]
-    }
-}
- 
-
- - -
-

- Request    - -    - -

-

- POST - api/v1/airlines/{airline_id}/flights -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-

Body Parameters

-
- flightnumber   -integer  -   -   - -
-

Numeric flight number, 1-4 digits. Example: 421

-
-
- departure_icao   -string  -   -   - -
-

Departure airport ICAO (must exist; uppercased). Example: EDDF

-
-
- arrival_icao   -string  -   -   - -
-

Arrival airport ICAO (must exist; uppercased). Example: EGLL

-
-
- aircraft_id   -integer  -   -   - -
-

ID of an active aircraft owned by the airline. Example: 7

-
-
- callsign   -string  -   -   - -
-

Radio callsign, 1-4 digits optionally followed by up to 2 letters. Example: 421

-
-
- crzalt   -integer  -   -   - -
-

Cruise altitude in feet, max 50000. Example: 36000

-
-
- blockoff   -string  -   -   - -
-

Block-off time (UTC), format Y-m-d H:i:s. Example: 2026-07-11 10:00:00

-
-
- blockon   -string  -   -   - -
-

Block-on time (UTC), format Y-m-d H:i:s. Example: 2026-07-11 11:30:00

-
-
- burned_fuel   -number  -   -   - -
-

Fuel burned. Example: 4200

-
-
- route   -string  -   -   - -
-

Filed route string. Example: SOVAT UL610 KONAN

-
-
- online_network_id   -integer  -   -   - -
-

Online network ID (must exist in online_networks). Example: 1

-
-
- remarks   -string  -optional   -   - -
-

Optional remarks (letters, digits, spaces, . , -). Example: Smooth flight.

-
-
- -

List the review queue

- -

-requires authentication -

- -

List pending PIREPs (status 1) for an airline. Requires the per-airline -Dispatcher or Manager role.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/airlines/16/flights/review" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/airlines/16/flights/review"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/airlines/16/flights/review';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": [
-        {
-            "id": 43,
-            "callsign": "422",
-            "flightNumber": "422",
-            "fullFlightNumber": "EV422",
-            "fullIcaoCallsign": "EVA422",
-            "departureIcao": "EGLL",
-            "arrivalIcao": "EDDF",
-            "cruiseAltitude": 37000,
-            "blockOff": "2026-07-11 13:00:00",
-            "blockOn": "2026-07-11 14:25:00",
-            "duration": "01:25",
-            "burnedFuel": 4100,
-            "route": "DET L6 KONAN",
-            "onlineNetwork": 1,
-            "status": {
-                "id": 1,
-                "name": "Pending"
-            },
-            "remarks": null,
-            "rejectionRemarks": null,
-            "createdAt": "2026-07-11T14:30:00.000000Z",
-            "updatedAt": "2026-07-11T14:30:00.000000Z"
-        }
-    ]
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "You do not have permission to review flights for this airline."
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/airlines/{airline_id}/flights/review -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- airline_id   -integer  -   -   - -
-

The ID of the airline. Example: 16

-
-
- airline   -integer  -   -   - -
-

The airline ID. Example: 1

-
-
- -

Accept a PIREP

- -

-requires authentication -

- -

Mark a pending PIREP as Accepted (status 2) and notify the pilot. -Requires the "review flight" permission for the flight's airline.

- - -
Example request:
- - -
-
curl --request POST \
-    "http://localhost:8000/api/v1/flights/16/accept" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/flights/16/accept"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "POST",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/flights/16/accept';
-$response = $client->post(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "id": 43,
-        "callsign": "422",
-        "flightNumber": "422",
-        "fullFlightNumber": "EV422",
-        "departureIcao": "EGLL",
-        "arrivalIcao": "EDDF",
-        "status": {
-            "id": 2,
-            "name": "Accepted"
-        },
-        "rejectionRemarks": null
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "This action is unauthorized."
-}
- 
-
- - -
-

- Request    - -    - -

-

- POST - api/v1/flights/{flight_id}/accept -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- flight_id   -integer  -   -   - -
-

The ID of the flight. Example: 16

-
-
- flight   -integer  -   -   - -
-

The flight ID. Example: 43

-
-
- -

Reject a PIREP

- -

-requires authentication -

- -

Mark a PIREP as Rejected (status 3) and notify the pilot. If location -continuity is on and the flight was still pending, the aircraft is moved -back to the flight's departure. Requires the "review flight" permission.

- - -
Example request:
- - -
-
curl --request POST \
-    "http://localhost:8000/api/v1/flights/16/reject" \
-    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json" \
-    --data "{
-    \"rejection_remarks\": \"Cruise altitude above aircraft ceiling.\"
-}"
-
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/flights/16/reject"
-);
-
-const headers = {
-    "Authorization": "Bearer {YOUR_AUTH_KEY}",
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-let body = {
-    "rejection_remarks": "Cruise altitude above aircraft ceiling."
-};
-
-fetch(url, {
-    method: "POST",
-    headers,
-    body: JSON.stringify(body),
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/flights/16/reject';
-$response = $client->post(
-    $url,
-    [
-        'headers' => [
-            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-        'json' => [
-            'rejection_remarks' => 'Cruise altitude above aircraft ceiling.',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "id": 43,
-        "callsign": "422",
-        "flightNumber": "422",
-        "fullFlightNumber": "EV422",
-        "departureIcao": "EGLL",
-        "arrivalIcao": "EDDF",
-        "status": {
-            "id": 3,
-            "name": "Rejected"
-        },
-        "rejectionRemarks": "Cruise altitude above aircraft ceiling."
-    }
-}
- 
-
-

Example response (403):

-
-
-
-{
-    "message": "This action is unauthorized."
-}
- 
-
- - -
-

- Request    - -    - -

-

- POST - api/v1/flights/{flight_id}/reject -

-

Headers

-
- Authorization   -  -   -   - -
-

Example: Bearer {YOUR_AUTH_KEY}

-
-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-

URL Parameters

-
- flight_id   -integer  -   -   - -
-

The ID of the flight. Example: 16

-
-
- flight   -integer  -   -   - -
-

The flight ID. Example: 43

-
-

Body Parameters

-
- rejection_remarks   -string  -optional   -   - -
-

Reason shown to the pilot. Example: Cruise altitude above aircraft ceiling.

-
-
- -

Instance

- -

Public metadata about this YAAMS instance.

- -

Instance info

- -

-

- -

Public instance metadata for API clients (e.g. ACARS "connect to your VA" -setup screens). Unauthenticated by design - exposes only what the public -landing page already shows.

- - -
Example request:
- - -
-
curl --request GET \
-    --get "http://localhost:8000/api/v1/info" \
-    --header "Content-Type: application/json" \
-    --header "Accept: application/json"
- - -
-
const url = new URL(
-    "http://localhost:8000/api/v1/info"
-);
-
-const headers = {
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-};
-
-
-fetch(url, {
-    method: "GET",
-    headers,
-}).then(response => response.json());
- - -
-
$client = new \GuzzleHttp\Client();
-$url = 'http://localhost:8000/api/v1/info';
-$response = $client->get(
-    $url,
-    [
-        'headers' => [
-            'Content-Type' => 'application/json',
-            'Accept' => 'application/json',
-        ],
-    ]
-);
-$body = $response->getBody();
-print_r(json_decode((string) $body));
- -
- - -
-

Example response (200):

-
-
-
-{
-    "data": {
-        "name": "Example Virtual Airlines",
-        "version": "1.1.0",
-        "apiVersion": "v1",
-        "supportEmail": "ops@example.com",
-        "features": {
-            "registration": true,
-            "userAirlineCreation": false
-        }
-    }
-}
- 
-
- - -
-

- Request    - -    - -

-

- GET - api/v1/info -

-

Headers

-
- Content-Type   -  -   -   - -
-

Example: application/json

-
-
- Accept   -  -   -   - -
-

Example: application/json

-
-
- - - - -
-
-
- - - -
-
-
- - diff --git a/routes/web.php b/routes/web.php index deb7347..0e2445b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,10 +26,11 @@ // Global Welcome / Guest landing page Route::get("/", [HomeController::class, "index"])->name("home"); -// Static Scribe API docs live at public/docs/index.html. In production nginx serves -// /docs directly (see Docker/nginx.conf); this redirect only fires under the dev -// `php artisan serve`, whose built-in server won't auto-serve a directory index. -Route::redirect('/docs', '/docs/index.html'); +// The Scribe API docs: a laravel-type Blade view (resources/views/scribe/index.blade.php, +// generated by `scribe:generate`) served publicly at /docs. Routing is manual (Scribe's +// own add_routes is off) so only this page is exposed - not the disabled Postman/OpenAPI +// spec routes. The base URL in the page resolves to the live APP_URL at render time. +Route::view('/docs', 'scribe.index')->name('scribe'); Route::middleware(['auth'])->group(function () {