Compare commits

...

11 Commits

29 changed files with 4518 additions and 447 deletions

View File

@@ -0,0 +1,44 @@
---
description: Run Open Ralph Wiggum loops in this repository
---
Run Open Ralph Wiggum in the current repository with OpenCode as the default agent.
**Input**: The argument after `/ralph` is the Ralph task prompt. Optional Ralph flags may be included.
**Steps**
1. **Require task input**
If no argument is provided, ask:
> "What task should Ralph run? Include success criteria and completion promise if you have one."
Stop after asking.
2. **Install Ralph locally in `.opencode`**
```bash
npm install --prefix ".opencode" @th0rgal/ralph-wiggum
```
3. **Run Ralph loop**
If the user did not provide iteration bounds, use:
```bash
npx --prefix ".opencode" ralph "<input>" --agent opencode --max-iterations 10
```
If the user included explicit flags (for example `--max-iterations`, `--tasks`, `--model`), preserve them and append `--agent opencode` only when agent is not specified.
4. **After run, show control commands**
```bash
npx --prefix ".opencode" ralph --status
npx --prefix ".opencode" ralph --add-context "Focus on <area>"
```
**Guardrails**
- Do not add `open-ralph-wiggum` to OpenCode plugin config.
- Keep the run scoped to this repository.
- Prefer bounded loops with `--max-iterations`.

41
.opencode/package-lock.json generated Normal file
View File

@@ -0,0 +1,41 @@
{
"name": ".opencode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.2.6",
"@th0rgal/ralph-wiggum": "^1.2.1"
}
},
"node_modules/@opencode-ai/plugin": {
"version": "1.2.6",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.2.6",
"zod": "4.1.8"
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.2.6",
"license": "MIT"
},
"node_modules/@th0rgal/ralph-wiggum": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@th0rgal/ralph-wiggum/-/ralph-wiggum-1.2.1.tgz",
"integrity": "sha512-8Xe6luwnKTArT9eBzyAx1newz+InGTBm9pCQrG4yiO9oYVHC0WZN3f1sQIpKwQbbnKQ4YJXdoraaatd0B4yDcA==",
"license": "MIT",
"bin": {
"ralph": "bin/ralph.js"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -0,0 +1,47 @@
---
name: open-ralph-wiggum
description: Run autonomous Ralph loops in the current repository using OpenCode as the default agent.
license: MIT
compatibility: Requires Node.js/npm and OpenCode CLI configured with a working model.
metadata:
author: th0rgal
source: https://github.com/Th0rgal/open-ralph-wiggum
---
Use Open Ralph Wiggum to run repeatable autonomous coding loops from OpenCode.
**Input**: The user message after this skill should include the task prompt for Ralph. Optional Ralph flags can be passed as part of the message.
**Steps**
1. **Validate a task prompt exists**
If no task prompt is provided, ask for one clear prompt and stop.
2. **Ensure Ralph CLI is available in this repo**
Run:
```bash
npm install --prefix ".opencode" @th0rgal/ralph-wiggum
```
3. **Run Ralph with OpenCode agent (default)**
Use local binary via `npx` so no global install is required:
```bash
npx --prefix ".opencode" ralph "<user prompt>" --agent opencode --max-iterations 10
```
If the user already included `--max-iterations`, keep their value.
4. **Report result and next controls**
Mention how to monitor or steer loop if still running:
- `npx --prefix ".opencode" ralph --status`
- `npx --prefix ".opencode" ralph --add-context "<hint>"`
**Guardrails**
- Do not register Ralph as an OpenCode plugin. It is a CLI wrapper, not a plugin.
- Keep execution in the current repository.
- Do not use unlimited iterations by default; always set `--max-iterations` unless user explicitly asks otherwise.

165
.ralph/ralph-history.json Normal file
View File

@@ -0,0 +1,165 @@
{
"iterations": [
{
"iteration": 1,
"startedAt": "2026-02-18T19:18:48.446Z",
"endedAt": "2026-02-18T19:18:53.115Z",
"durationMs": 1302,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [
".opencode/skills/open-ralph-wiggum/",
".ralph/"
],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 2,
"startedAt": "2026-02-18T19:18:57.638Z",
"endedAt": "2026-02-18T19:19:02.087Z",
"durationMs": 1135,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 3,
"startedAt": "2026-02-18T19:19:06.524Z",
"endedAt": "2026-02-18T19:19:11.037Z",
"durationMs": 1212,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 4,
"startedAt": "2026-02-18T19:19:15.470Z",
"endedAt": "2026-02-18T19:19:19.996Z",
"durationMs": 1234,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 5,
"startedAt": "2026-02-18T19:19:24.447Z",
"endedAt": "2026-02-18T19:19:28.947Z",
"durationMs": 1167,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 6,
"startedAt": "2026-02-18T19:19:33.395Z",
"endedAt": "2026-02-18T19:19:37.864Z",
"durationMs": 1159,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 7,
"startedAt": "2026-02-18T19:19:42.294Z",
"endedAt": "2026-02-18T19:19:46.806Z",
"durationMs": 1172,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 8,
"startedAt": "2026-02-18T19:19:51.271Z",
"endedAt": "2026-02-18T19:19:55.730Z",
"durationMs": 1147,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 9,
"startedAt": "2026-02-18T19:20:00.161Z",
"endedAt": "2026-02-18T19:20:04.602Z",
"durationMs": 1153,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
},
{
"iteration": 10,
"startedAt": "2026-02-18T19:20:09.028Z",
"endedAt": "2026-02-18T19:20:13.478Z",
"durationMs": 1149,
"agent": "opencode",
"model": "",
"toolsUsed": {},
"filesModified": [],
"exitCode": 1,
"completionDetected": false,
"errors": [
"\u001b[91m\u001b[1mError: \u001b[0mSession not found"
]
}
],
"totalDurationMs": 11830,
"struggleIndicators": {
"repeatedErrors": {
"\u001b[91m\u001b[1mError: \u001b[0mSession not found": 10
},
"noProgressIterations": 9,
"shortIterations": 10
}
}

View File

@@ -0,0 +1,13 @@
{
"active": true,
"iteration": 10,
"minIterations": 1,
"maxIterations": 10,
"completionPromise": "COMPLETE",
"tasksMode": false,
"taskPromise": "READY_FOR_NEXT_TASK",
"prompt": "Test the changes made by running the scribe command and the swagger is working fine. In case any issues found, fix and retest until the issue is resolved. once that is done, /opsx-verify, /opsx-sync and /opsx-archive. Then commit the code. Attempt a push, if failed, leave it for me. <promise>DONE</promise> when complete.",
"startedAt": "2026-02-18T19:18:44.320Z",
"model": "",
"agent": "opencode"
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"read": "allow",
"edit": "allow",
"glob": "allow",
"grep": "allow",
"list": "allow",
"bash": "allow",
"task": "allow",
"webfetch": "allow",
"websearch": "allow",
"codesearch": "allow",
"todowrite": "allow",
"todoread": "allow",
"question": "allow",
"lsp": "allow",
"external_directory": "allow"
}
}

View File

@@ -0,0 +1,4 @@
# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
# Scribe uses this file to know when you change something manually in your docs.
.scribe/intro.md=63d14186b9cbbb0a80ee87cd913db091
.scribe/auth.md=5c5a140c89034600ae349aede2a22ec8

7
backend/.scribe/auth.md Normal file
View File

@@ -0,0 +1,7 @@
# Authenticating requests
To authenticate requests, include an **`Authorization`** header with the value **`"Bearer Bearer {token}"`**.
All authenticated endpoints are marked with a `requires authentication` badge in the documentation below.
Get tokens from `POST /api/auth/login`, send access token as `Bearer {token}`, and renew with `POST /api/auth/refresh` before access token expiry.

View File

@@ -0,0 +1,226 @@
## Autogenerated by Scribe. DO NOT MODIFY.
name: Authentication
description: |-
Endpoints for JWT authentication and session lifecycle.
endpoints:
-
custom: []
httpMethods:
- POST
uri: api/auth/login
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Login and get tokens'
description: 'Authenticate with email and password to receive an access token and refresh token.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
email:
custom: []
name: email
description: 'User email address.'
required: true
example: user@example.com
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
password:
custom: []
name: password
description: 'User password.'
required: true
example: secret123
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
email: user@example.com
password: secret123
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "abc123def456",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice Johnson",
"email": "user@example.com",
"role": "manager"
}
}
headers: []
description: ''
-
custom: []
status: 401
content: '{"message":"Invalid credentials"}'
headers: []
description: ''
-
custom: []
status: 403
content: '{"message":"Account is inactive"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"errors":{"email":["The email field is required."],"password":["The password field is required."]}}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/auth/refresh
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Refresh access token'
description: 'Exchange a valid refresh token for a new access token and refresh token pair.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
refresh_token:
custom: []
name: refresh_token
description: 'Refresh token returned by login.'
required: true
example: abc123def456
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
refresh_token: abc123def456
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "newtoken123",
"token_type": "bearer",
"expires_in": 3600
}
headers: []
description: ''
-
custom: []
status: 401
content: '{"message":"Invalid or expired refresh token"}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/auth/logout
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Logout current session'
description: 'Invalidate a refresh token and end the active authenticated session.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
refresh_token:
custom: []
name: refresh_token
description: 'Optional refresh token to invalidate immediately.'
required: false
example: abc123def456
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
refresh_token: abc123def456
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"message":"Logged out successfully"}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null

View File

@@ -0,0 +1,224 @@
name: Authentication
description: |-
Endpoints for JWT authentication and session lifecycle.
endpoints:
-
custom: []
httpMethods:
- POST
uri: api/auth/login
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Login and get tokens'
description: 'Authenticate with email and password to receive an access token and refresh token.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
email:
custom: []
name: email
description: 'User email address.'
required: true
example: user@example.com
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
password:
custom: []
name: password
description: 'User password.'
required: true
example: secret123
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
email: user@example.com
password: secret123
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "abc123def456",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice Johnson",
"email": "user@example.com",
"role": "manager"
}
}
headers: []
description: ''
-
custom: []
status: 401
content: '{"message":"Invalid credentials"}'
headers: []
description: ''
-
custom: []
status: 403
content: '{"message":"Account is inactive"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"errors":{"email":["The email field is required."],"password":["The password field is required."]}}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/auth/refresh
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Refresh access token'
description: 'Exchange a valid refresh token for a new access token and refresh token pair.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
refresh_token:
custom: []
name: refresh_token
description: 'Refresh token returned by login.'
required: true
example: abc123def456
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
refresh_token: abc123def456
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "newtoken123",
"token_type": "bearer",
"expires_in": 3600
}
headers: []
description: ''
-
custom: []
status: 401
content: '{"message":"Invalid or expired refresh token"}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/auth/logout
metadata:
custom: []
groupName: Authentication
groupDescription: |-
Endpoints for JWT authentication and session lifecycle.
subgroup: ''
subgroupDescription: ''
title: 'Logout current session'
description: 'Invalidate a refresh token and end the active authenticated session.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer Bearer {token}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
refresh_token:
custom: []
name: refresh_token
description: 'Optional refresh token to invalidate immediately.'
required: false
example: abc123def456
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
refresh_token: abc123def456
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"message":"Logged out successfully"}'
headers: []
description: ''
responseFields: []
auth:
- headers
- Authorization
- 'Bearer Bearer {token}'
controller: null
method: null
route: null

View File

@@ -0,0 +1,53 @@
# To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
# you can define it in a custom.*.yaml file, like this one.
# Each custom file should contain an array of endpoints. Here's an example:
# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options
#- httpMethods:
# - POST
# uri: api/doSomething/{param}
# metadata:
# groupName: The group the endpoint belongs to. Can be a new group or an existing group.
# groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
# subgroup: You can add a subgroup, too.
# title: Do something
# description: 'This endpoint allows you to do something.'
# authenticated: false
# headers:
# Content-Type: application/json
# Accept: application/json
# urlParameters:
# param:
# name: param
# description: A URL param for no reason.
# required: true
# example: 2
# type: integer
# queryParameters:
# speed:
# name: speed
# description: How fast the thing should be done. Can be `slow` or `fast`.
# required: false
# example: fast
# type: string
# bodyParameters:
# something:
# name: something
# description: The things we should do.
# required: true
# example:
# - string 1
# - string 2
# type: 'string[]'
# responses:
# - status: 200
# description: 'When the thing was done smoothly.'
# content: # Your response content can be an object, an array, a string or empty.
# {
# "hey": "ho ho ho"
# }
# responseFields:
# hey:
# name: hey
# description: Who knows?
# type: string # This is optional

12
backend/.scribe/intro.md Normal file
View File

@@ -0,0 +1,12 @@
# Introduction
Resource planning and capacity management API
<aside>
<strong>Base URL</strong>: <code>http://localhost/api</code>
</aside>
Authenticate by sending `Authorization: Bearer {access_token}` on protected endpoints.
Access tokens are valid for 60 minutes. Use `/api/auth/refresh` with your refresh token to obtain a new access token and refresh token pair.

View File

@@ -10,8 +10,37 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
/**
* @group Authentication
*
* Endpoints for JWT authentication and session lifecycle.
*/
class AuthController extends Controller
{
/**
* Login and get tokens
*
* Authenticate with email and password to receive an access token and refresh token.
*
* @bodyParam email string required User email address. Example: user@example.com
* @bodyParam password string required User password. Example: secret123
*
* @response 200 {
* "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
* "refresh_token": "abc123def456",
* "token_type": "bearer",
* "expires_in": 3600,
* "user": {
* "id": "550e8400-e29b-41d4-a716-446655440000",
* "name": "Alice Johnson",
* "email": "user@example.com",
* "role": "manager"
* }
* }
* @response 401 {"message":"Invalid credentials"}
* @response 403 {"message":"Account is inactive"}
* @response 422 {"errors":{"email":["The email field is required."],"password":["The password field is required."]}}
*/
public function login(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
@@ -56,6 +85,22 @@ class AuthController extends Controller
]);
}
/**
* Refresh access token
*
* Exchange a valid refresh token for a new access token and refresh token pair.
*
* @authenticated
* @bodyParam refresh_token string required Refresh token returned by login. Example: abc123def456
*
* @response 200 {
* "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
* "refresh_token": "newtoken123",
* "token_type": "bearer",
* "expires_in": 3600
* }
* @response 401 {"message":"Invalid or expired refresh token"}
*/
public function refresh(Request $request): JsonResponse
{
$refreshToken = $request->input('refresh_token');
@@ -89,6 +134,16 @@ class AuthController extends Controller
]);
}
/**
* Logout current session
*
* Invalidate a refresh token and end the active authenticated session.
*
* @authenticated
* @bodyParam refresh_token string Optional refresh token to invalidate immediately. Example: abc123def456
*
* @response 200 {"message":"Logged out successfully"}
*/
public function logout(Request $request): JsonResponse
{
$user = $request->user();

View File

@@ -7,8 +7,10 @@
"license": "MIT",
"require": {
"php": "^8.2",
"knuckleswtf/scribe": "^5.7",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"parsedown/parsedown": "1.7.4",
"predis/predis": "^2.0",
"tymon/jwt-auth": "^2.0"
},

974
backend/composer.lock generated

File diff suppressed because it is too large Load Diff

24
backend/config/cors.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
return [
'paths' => [
'api/*',
'api/documentation',
'api/documentation/*',
'sanctum/csrf-cookie',
],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

251
backend/config/scribe.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
use Knuckles\Scribe\Config\AuthIn;
use Knuckles\Scribe\Config\Defaults;
use Knuckles\Scribe\Extracting\Strategies;
use function Knuckles\Scribe\Config\removeStrategies;
// Only the most common configs are shown. See the https://scribe.knuckles.wtf/laravel/reference/config for all.
return [
// The HTML <title> for the generated documentation.
'title' => 'Headroom API',
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
'description' => 'Resource planning and capacity management API',
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
'intro_text' => <<<'INTRO'
Authenticate by sending `Authorization: Bearer {access_token}` on protected endpoints.
Access tokens are valid for 60 minutes. Use `/api/auth/refresh` with your refresh token to obtain a new access token and refresh token pair.
INTRO,
// 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' => rtrim(config('app.url'), '/').'/api',
// Routes to include in the docs
'routes' => [
[
'match' => [
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
'prefixes' => ['api/*'],
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
'domains' => ['*'],
],
// Include these routes even if they did not match the rules above.
'include' => [
// 'users.index', 'POST /new', '/auth/*'
],
// Exclude these routes even if they matched the rules above.
'exclude' => [
'api/user',
],
],
],
// The type of documentation output to generate.
// - "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' => 'laravel',
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
'theme' => 'default',
'static' => [
// HTML documentation, assets and Postman collection will be generated to this folder.
// Source Markdown will still be in resources/docs.
'output_path' => 'public/docs',
],
'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,
// 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.
'docs_url' => '/api/documentation',
// Directory within `public` in which to store CSS and JS assets.
// By default, assets are stored in `public/vendor/scribe`.
// If set, assets will be stored in `public/{{assets_directory}}`
'assets_directory' => null,
// Middleware to attach to the docs endpoint (if `add_routes` is true).
'middleware' => [],
],
'external' => [
'html_attributes' => [],
],
'try_it_out' => [
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
// Don't forget to enable CORS headers for your endpoints.
'enabled' => true,
// The base URL to use in the API tester. Leave as null to be the same as the displayed URL (`scribe.base_url`).
'base_url' => null,
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
'use_csrf' => false,
// The URL to fetch the CSRF token from (if `use_csrf` is true).
'csrf_url' => '/sanctum/csrf-cookie',
],
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
'auth' => [
// Set this to true if ANY endpoints in your API use authentication.
'enabled' => true,
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
'default' => true,
// Where is the auth value meant to be sent in a request?
'in' => AuthIn::BEARER->value,
// The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key).
'name' => 'Authorization',
// The value of the parameter to be used by Scribe to authenticate response calls.
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
'use_value' => 'Bearer {token}',
// Placeholder your users will see for the auth parameter in the example requests.
// Set this to null if you want Scribe to use a random value as placeholder instead.
'placeholder' => 'Bearer {token}',
// Any extra authentication-related info for your users. Markdown and HTML are supported.
'extra_info' => 'Get tokens from `POST /api/auth/login`, send access token as `Bearer {token}`, and renew with `POST /api/auth/refresh` before access token expiry.',
],
// Example requests for each endpoint will be shown in each of these languages.
// Supported options are: bash, javascript, php, python
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
// Note: does not work for `external` docs types
'example_languages' => [
'bash',
'javascript',
],
// 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' => [
'enabled' => true,
'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.
'openapi' => [
'enabled' => true,
// 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.
// See https://spec.openapis.org/oas/v3.1.0 for details on 3.1 changes.
'version' => '3.0.3',
'overrides' => [
// 'info.version' => '2.0.0',
],
// Additional generators to use when generating the OpenAPI spec.
// Should extend `Knuckles\Scribe\Writing\OpenApiSpecGenerators\OpenApiGenerator`.
'generators' => [],
],
'groups' => [
// Endpoints which don't have a @group will be placed in this default group.
'default' => 'Endpoints',
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
// Note: does not work for `external` docs types
'order' => [],
],
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
// For example, if your logo is in public/img:
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
// - 'logo' => 'img/logo.png' // for `laravel` type
'logo' => false,
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
// Examples:
// - {date:F j Y} => March 28, 2022
// - {git:short} => Short hash of the last Git commit
// Available tokens are `{date:<format>}` and `{git:<format>}`.
// The format you pass to `date` will be passed to PHP's `date()` function.
// The format you pass to `git` can be either "short" or "long".
// Note: does not work for `external` docs types
'last_updated' => 'Last updated: {date:F j, Y}',
'examples' => [
// Set this to any number to generate the same example values for parameters on each run,
'faker_seed' => 1234,
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
// You can reorder or remove strategies here.
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
],
// The strategies Scribe will use to extract information about your routes at each stage.
// Use configureStrategy() to specify settings for a strategy in the list.
// Use removeStrategies() to remove an included strategy.
'strategies' => [
'metadata' => [
...Defaults::METADATA_STRATEGIES,
],
'headers' => [
...Defaults::HEADERS_STRATEGIES,
Strategies\StaticData::withSettings(data: [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]),
],
'urlParameters' => [
...Defaults::URL_PARAMETERS_STRATEGIES,
],
'queryParameters' => [
...Defaults::QUERY_PARAMETERS_STRATEGIES,
],
'bodyParameters' => [
...Defaults::BODY_PARAMETERS_STRATEGIES,
],
'responses' => removeStrategies(
Defaults::RESPONSES_STRATEGIES,
[Strategies\Responses\ResponseCalls::class],
),
'responseFields' => [
...Defaults::RESPONSE_FIELDS_STRATEGIES,
],
],
// For response calls, API resource responses and transformer responses,
// Scribe will try to start database transactions, so no changes are persisted to your database.
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
'database_connections_to_transact' => [config('database.default')],
'fractal' => [
// If you are using a custom serializer with league/fractal, you can specify it here.
'serializer' => null,
],
];

View File

@@ -0,0 +1,393 @@
/* Copied from https://github.com/slatedocs/slate/blob/c4b4c0b8f83e891ca9fab6bbe9a1a88d5fe41292/stylesheets/print.css and unminified */
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block
}
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline
}
audio:not([controls]) {
display: none;
height: 0
}
[hidden],
template {
display: none
}
a {
background-color: transparent
}
a:active,
a:hover {
outline: 0
}
abbr[title] {
border-bottom: 1px dotted
}
b,
strong {
font-weight: bold
}
dfn {
font-style: italic
}
h1 {
font-size: 2em;
margin: 0.67em 0
}
mark {
background: #ff0;
color: #000
}
small {
font-size: 80%
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sup {
top: -0.5em
}
sub {
bottom: -0.25em
}
img {
border: 0
}
svg:not(:root) {
overflow: hidden
}
figure {
margin: 1em 40px
}
hr {
box-sizing: content-box;
height: 0
}
pre {
overflow: auto
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0
}
button {
overflow: visible
}
button,
select {
text-transform: none
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button;
cursor: pointer
}
button[disabled],
html input[disabled] {
cursor: default
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0
}
input {
line-height: normal
}
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box;
padding: 0
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto
}
input[type="search"] {
-webkit-appearance: textfield;
box-sizing: content-box
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em
}
legend {
border: 0;
padding: 0
}
textarea {
overflow: auto
}
optgroup {
font-weight: bold
}
table {
border-collapse: collapse;
border-spacing: 0
}
td,
th {
padding: 0
}
.content h1,
.content h2,
.content h3,
.content h4,
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px
}
.content h1,
.content h2,
.content h3,
.content h4 {
font-weight: bold
}
.content pre,
.content code {
font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
font-size: 12px;
line-height: 1.5
}
.content pre,
.content code {
word-break: break-all;
-webkit-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto
}
@font-face {
font-family: 'slate';
src: url(../fonts/slate.eot?-syv14m);
src: url(../fonts/slate.eot?#iefix-syv14m) format("embedded-opentype"), url(../fonts/slate.woff2?-syv14m) format("woff2"), url(../fonts/slate.woff?-syv14m) format("woff"), url(../fonts/slate.ttf?-syv14m) format("truetype"), url(../fonts/slate.svg?-syv14m#slate) format("svg");
font-weight: normal;
font-style: normal
}
.content aside.warning:before,
.content aside.notice:before,
.content aside.success:before {
font-family: 'slate';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1
}
.content aside.warning:before {
content: "\e600"
}
.content aside.notice:before {
content: "\e602"
}
.content aside.success:before {
content: "\e606"
}
.tocify,
.toc-footer,
.lang-selector,
.search,
#nav-button {
display: none
}
.tocify-wrapper>img {
margin: 0 auto;
display: block
}
.content {
font-size: 12px
}
.content pre,
.content code {
border: 1px solid #999;
border-radius: 5px;
font-size: 0.8em
}
.content pre code {
border: 0
}
.content pre {
padding: 1.3em
}
.content code {
padding: 0.2em
}
.content table {
border: 1px solid #999
}
.content table tr {
border-bottom: 1px solid #999
}
.content table td,
.content table th {
padding: 0.7em
}
.content p {
line-height: 1.5
}
.content a {
text-decoration: none;
color: #000
}
.content h1 {
font-size: 2.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-top: 1em;
margin-bottom: 21px;
border: 2px solid #ccc;
border-width: 2px 0;
text-align: center
}
.content h2 {
font-size: 1.8em;
margin-top: 2em;
border-top: 2px solid #ccc;
padding-top: 0.8em
}
.content h1+h2,
.content h1+div+h2 {
border-top: none;
padding-top: 0;
margin-top: 0
}
.content h3,
.content h4 {
font-size: 0.8em;
margin-top: 1.5em;
margin-bottom: 0.8em;
text-transform: uppercase
}
.content h5,
.content h6 {
text-transform: uppercase
}
.content aside {
padding: 1em;
border: 1px solid #ccc;
border-radius: 5px;
margin-top: 1.5em;
margin-bottom: 1.5em;
line-height: 1.6
}
.content aside:before {
vertical-align: middle;
padding-right: 0.5em;
font-size: 14px
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@@ -0,0 +1,149 @@
document.addEventListener('DOMContentLoaded', function() {
const updateHash = function (id) {
window.location.hash = `#${id}`;
};
const navButton = document.getElementById('nav-button');
const menuWrapper = document.querySelector('.tocify-wrapper');
function toggleSidebar(event) {
event.preventDefault();
if (menuWrapper) {
menuWrapper.classList.toggle('open');
navButton.classList.toggle('open');
}
}
function closeSidebar() {
if (menuWrapper) {
menuWrapper.classList.remove('open');
navButton.classList.remove('open');
}
}
navButton.addEventListener('click', toggleSidebar);
window.hljs.highlightAll();
const wrapper = document.getElementById('toc');
// https://jets.js.org/
window.jets = new window.Jets({
// *OR - Selects elements whose values contains at least one part of search substring
searchSelector: '*OR',
searchTag: '#input-search',
contentTag: '#toc li',
didSearch: function(term) {
wrapper.classList.toggle('jets-searching', String(term).length > 0)
},
// map these accent keys to plain values
diacriticsMap: {
a: 'ÀÁÂÃÄÅàáâãäåĀāąĄ',
c: 'ÇçćĆčČ',
d: 'đĐďĎ',
e: 'ÈÉÊËèéêëěĚĒēęĘ',
i: 'ÌÍÎÏìíîïĪī',
l: 'łŁ',
n: 'ÑñňŇńŃ',
o: 'ÒÓÔÕÕÖØòóôõöøŌō',
r: 'řŘ',
s: 'ŠšśŚ',
t: 'ťŤ',
u: 'ÙÚÛÜùúûüůŮŪū',
y: 'ŸÿýÝ',
z: 'ŽžżŻźŹ'
}
});
function hashChange() {
const currentItems = document.querySelectorAll('.tocify-subheader.visible, .tocify-item.tocify-focus');
Array.from(currentItems).forEach((elem) => {
elem.classList.remove('visible', 'tocify-focus');
});
const currentTag = document.querySelector(`a[href="${window.location.hash}"]`);
if (currentTag) {
const parent = currentTag.closest('.tocify-subheader');
if (parent) {
parent.classList.add('visible');
}
const siblings = currentTag.closest('.tocify-header');
if (siblings) {
Array.from(siblings.querySelectorAll('.tocify-subheader')).forEach((elem) => {
elem.classList.add('visible');
});
}
currentTag.parentElement.classList.add('tocify-focus');
// wait for dom changes to be done
setTimeout(() => {
currentTag.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
// only close the sidebar on level-2 events
if (currentTag.parentElement.classList.contains('level-2')) {
closeSidebar();
}
}, 1500);
}
}
let languages = JSON.parse(document.body.getAttribute('data-languages'));
// Support a key => value object where the key is the name, or an array of strings where the value is the name
if (!Array.isArray(languages)) {
languages = Object.values(languages);
}
// if there is no language use the first one
const currentLanguage = window.localStorage.getItem('language') || languages[0];
const languageStyle = document.getElementById('language-style');
const langSelector = document.querySelectorAll('.lang-selector button.lang-button');
function setActiveLanguage(newLanguage) {
window.localStorage.setItem('language', newLanguage);
if (!languageStyle) {
return;
}
const newStyle = languages.map((language) => {
return language === newLanguage
// the current one should be visible
? `body .content .${language}-example pre { display: block; }`
// the inactive one should be hidden
: `body .content .${language}-example pre { display: none; }`;
}).join(`\n`);
Array.from(langSelector).forEach((elem) => {
elem.classList.toggle('active', elem.getAttribute('data-language-name') === newLanguage);
});
const activeHash = window.location.hash.slice(1);
languageStyle.innerHTML = newStyle;
setTimeout(() => {
updateHash(activeHash);
}, 200);
}
setActiveLanguage(currentLanguage);
Array.from(langSelector).forEach((elem) => {
elem.addEventListener('click', () => {
const newLanguage = elem.getAttribute('data-language-name');
setActiveLanguage(newLanguage);
});
});
window.addEventListener('hashchange', hashChange, false);
const divs = document.querySelectorAll('.content h1[id], .content h2[id]');
document.addEventListener('scroll', () => {
divs.forEach(item => {
const rect = item.getBoundingClientRect();
if (rect.top > 0 && rect.top < 150) {
const location = window.location.toString().split('#')[0];
history.replaceState(null, null, location + '#' + item.id);
hashChange();
}
});
});
hashChange();
});

View File

@@ -0,0 +1,289 @@
window.abortControllers = {};
function cacheAuthValue() {
// Whenever the auth header is set for one endpoint, cache it for the others
window.lastAuthValue = '';
let authInputs = document.querySelectorAll(`.auth-value`)
authInputs.forEach(el => {
el.addEventListener('input', (event) => {
window.lastAuthValue = event.target.value;
authInputs.forEach(otherInput => {
if (otherInput === el) return;
// Don't block the main thread
setTimeout(() => {
otherInput.value = window.lastAuthValue;
}, 0);
});
});
});
}
window.addEventListener('DOMContentLoaded', cacheAuthValue);
function getCookie(name) {
if (!document.cookie) {
return null;
}
const cookies = document.cookie.split(';')
.map(c => c.trim())
.filter(c => c.startsWith(name + '='));
if (cookies.length === 0) {
return null;
}
return decodeURIComponent(cookies[0].split('=')[1]);
}
function tryItOut(endpointId) {
document.querySelector(`#btn-tryout-${endpointId}`).hidden = true;
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = false;
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`).hidden = false;
executeBtn.disabled = false;
// Show all input fields
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
.forEach(el => el.style.display = 'block');
if (document.querySelector(`#form-${endpointId}`).dataset.authed === "1") {
const authElement = document.querySelector(`#auth-${endpointId}`);
authElement && (authElement.hidden = false);
}
// Expand all nested fields
document.querySelectorAll(`#form-${endpointId} details`)
.forEach(el => el.open = true);
}
function cancelTryOut(endpointId) {
if (window.abortControllers[endpointId]) {
window.abortControllers[endpointId].abort();
delete window.abortControllers[endpointId];
}
document.querySelector(`#btn-tryout-${endpointId}`).hidden = false;
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
executeBtn.hidden = true;
executeBtn.textContent = executeBtn.dataset.initialText;
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = true;
// Hide inputs
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
.forEach(el => el.style.display = 'none');
document.querySelectorAll(`#form-${endpointId} details`)
.forEach(el => el.open = false);
const authElement = document.querySelector(`#auth-${endpointId}`);
authElement && (authElement.hidden = true);
document.querySelector('#execution-results-' + endpointId).hidden = true;
document.querySelector('#execution-error-' + endpointId).hidden = true;
// Revert to sample code blocks
document.querySelector('#example-requests-' + endpointId).hidden = false;
document.querySelector('#example-responses-' + endpointId).hidden = false;
}
function makeAPICall(method, path, body = {}, query = {}, headers = {}, endpointId = null) {
console.log({endpointId, path, body, query, headers});
if (!(body instanceof FormData) && typeof body !== "string") {
body = JSON.stringify(body)
}
const url = new URL(window.tryItOutBaseUrl + '/' + path.replace(/^\//, ''));
// We need this function because if you try to set an array or object directly to a URLSearchParams object,
// you'll get [object Object] or the array.toString()
function addItemToSearchParamsObject(key, value, searchParams) {
if (Array.isArray(value)) {
value.forEach((v, i) => {
// Append {filters: [first, second]} as filters[0]=first&filters[1]second
addItemToSearchParamsObject(key + '[' + i + ']', v, searchParams);
})
} else if (typeof value === 'object' && value !== null) {
Object.keys(value).forEach((i) => {
// Append {filters: {name: first}} as filters[name]=first
addItemToSearchParamsObject(key + '[' + i + ']', value[i], searchParams);
});
} else {
searchParams.append(key, value);
}
}
Object.keys(query)
.forEach(key => addItemToSearchParamsObject(key, query[key], url.searchParams));
window.abortControllers[endpointId] = new AbortController();
return fetch(url, {
method,
headers,
body: method === 'GET' ? undefined : body,
signal: window.abortControllers[endpointId].signal,
referrer: window.tryItOutBaseUrl,
mode: 'cors',
credentials: 'same-origin',
})
.then(response => Promise.all([response.status, response.statusText, response.text(), response.headers]));
}
function hideCodeSamples(endpointId) {
document.querySelector('#example-requests-' + endpointId).hidden = true;
document.querySelector('#example-responses-' + endpointId).hidden = true;
}
function handleResponse(endpointId, response, status, headers) {
hideCodeSamples(endpointId);
// Hide error views
document.querySelector('#execution-error-' + endpointId).hidden = true;
const responseContentEl = document.querySelector('#execution-response-content-' + endpointId);
// Check if the response contains Laravel's dd() default dump output
const isLaravelDump = response.includes('Sfdump');
// If it's a Laravel dd() dump, use innerHTML to render it safely
if (isLaravelDump) {
responseContentEl.innerHTML = response === '' ? responseContentEl.dataset.emptyResponseText : response;
} else {
// Otherwise, stick to textContent for regular responses
responseContentEl.textContent = response === '' ? responseContentEl.dataset.emptyResponseText : response;
}
// Prettify it if it's JSON
let isJson = false;
try {
const jsonParsed = JSON.parse(response);
if (jsonParsed !== null) {
isJson = true;
response = JSON.stringify(jsonParsed, null, 4);
responseContentEl.textContent = response;
}
} catch (e) {
}
isJson && window.hljs.highlightElement(responseContentEl);
const statusEl = document.querySelector('#execution-response-status-' + endpointId);
statusEl.textContent = ` (${status})`;
document.querySelector('#execution-results-' + endpointId).hidden = false;
statusEl.scrollIntoView({behavior: "smooth", block: "center"});
}
function handleError(endpointId, err) {
hideCodeSamples(endpointId);
// Hide response views
document.querySelector('#execution-results-' + endpointId).hidden = true;
// Show error views
let errorMessage = err.message || err;
const $errorMessageEl = document.querySelector('#execution-error-message-' + endpointId);
$errorMessageEl.textContent = errorMessage + $errorMessageEl.textContent;
const errorEl = document.querySelector('#execution-error-' + endpointId);
errorEl.hidden = false;
errorEl.scrollIntoView({behavior: "smooth", block: "center"});
}
async function executeTryOut(endpointId, form) {
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
executeBtn.textContent = executeBtn.dataset.loadingText;
executeBtn.disabled = true;
executeBtn.scrollIntoView({behavior: "smooth", block: "center"});
let body;
let setter;
if (form.dataset.hasfiles === "1") {
body = new FormData();
setter = (name, value) => body.append(name, value);
} else if (form.dataset.isarraybody === "1") {
body = [];
setter = (name, value) => _.set(body, name, value);
} else {
body = {};
setter = (name, value) => _.set(body, name, value);
}
const bodyParameters = form.querySelectorAll('input[data-component=body]');
bodyParameters.forEach(el => {
let value = el.value;
if (el.type === 'number' && typeof value === 'string') {
value = parseFloat(value);
}
if (el.type === 'file' && el.files[0]) {
setter(el.name, el.files[0]);
return;
}
if (el.type !== 'radio') {
if (value === "" && el.required === false) {
// Don't include empty optional values in the request
return;
}
setter(el.name, value);
return;
}
if (el.checked) {
value = (value === 'false') ? false : true;
setter(el.name, value);
}
});
const query = {};
const queryParameters = form.querySelectorAll('input[data-component=query]');
queryParameters.forEach(el => {
if (el.type !== 'radio' || (el.type === 'radio' && el.checked)) {
if (el.value === '') {
// Don't include empty values in the request
return;
}
_.set(query, el.name, el.value);
}
});
let path = form.dataset.path;
const urlParameters = form.querySelectorAll('input[data-component=url]');
urlParameters.forEach(el => (path = path.replace(new RegExp(`\\{${el.name}\\??}`), el.value)));
const headers = Object.fromEntries(Array.from(form.querySelectorAll('input[data-component=header]'))
.map(el => [el.name, el.value]));
// When using FormData, the browser sets the correct content-type + boundary
let method = form.dataset.method;
if (body instanceof FormData) {
delete headers['Content-Type'];
// When using FormData with PUT or PATCH, use method spoofing so PHP can access the post body
if (['PUT', 'PATCH'].includes(form.dataset.method)) {
method = 'POST';
setter('_method', form.dataset.method);
}
}
let preflightPromise = Promise.resolve();
if (window.useCsrf && window.csrfUrl) {
preflightPromise = makeAPICall('GET', window.csrfUrl).then(() => {
headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
});
}
return preflightPromise.then(() => makeAPICall(method, path, body, query, headers, endpointId))
.then(([responseStatus, statusText, responseContent, responseHeaders]) => {
handleResponse(endpointId, responseContent, responseStatus, responseHeaders)
})
.catch(err => {
if (err.name === "AbortError") {
console.log("Request cancelled");
return;
}
console.log("Error while making request: ", err);
handleError(endpointId, err);
})
.finally(() => {
executeBtn.disabled = false;
executeBtn.textContent = executeBtn.dataset.initialText;
});
}

View File

@@ -0,0 +1,668 @@
<!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>Headroom API</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ asset("/vendor/scribe/css/theme-default.style.css") }}" media="screen">
<link rel="stylesheet" href="{{ asset("/vendor/scribe/css/theme-default.print.css") }}" media="print">
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>
<link rel="stylesheet"
href="https://unpkg.com/@highlightjs/cdn-assets@11.6.0/styles/obsidian.min.css">
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.6.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jets/0.14.1/jets.min.js"></script>
<style id="language-style">
/* starts out as display none and is replaced with js later */
body .content .bash-example code { display: none; }
body .content .javascript-example code { display: none; }
</style>
<script>
var tryItOutBaseUrl = "http://localhost/api";
var useCsrf = Boolean();
var csrfUrl = "/sanctum/csrf-cookie";
</script>
<script src="{{ asset("/vendor/scribe/js/tryitout-5.7.0.js") }}"></script>
<script src="{{ asset("/vendor/scribe/js/theme-default-5.7.0.js") }}"></script>
</head>
<body data-languages="[&quot;bash&quot;,&quot;javascript&quot;]">
<a href="#" id="nav-button">
<span>
MENU
<img src="{{ asset("/vendor/scribe/images/navbar.png") }}" alt="navbar-image"/>
</span>
</a>
<div class="tocify-wrapper">
<div class="lang-selector">
<button type="button" class="lang-button" data-language-name="bash">bash</button>
<button type="button" class="lang-button" data-language-name="javascript">javascript</button>
</div>
<div class="search">
<input type="text" class="search" id="input-search" placeholder="Search">
</div>
<div id="toc">
<ul id="tocify-header-introduction" class="tocify-header">
<li class="tocify-item level-1" data-unique="introduction">
<a href="#introduction">Introduction</a>
</li>
</ul>
<ul id="tocify-header-authenticating-requests" class="tocify-header">
<li class="tocify-item level-1" data-unique="authenticating-requests">
<a href="#authenticating-requests">Authenticating requests</a>
</li>
</ul>
<ul id="tocify-header-authentication" class="tocify-header">
<li class="tocify-item level-1" data-unique="authentication">
<a href="#authentication">Authentication</a>
</li>
<ul id="tocify-subheader-authentication" class="tocify-subheader">
<li class="tocify-item level-2" data-unique="authentication-POSTapi-auth-login">
<a href="#authentication-POSTapi-auth-login">Login and get tokens</a>
</li>
<li class="tocify-item level-2" data-unique="authentication-POSTapi-auth-refresh">
<a href="#authentication-POSTapi-auth-refresh">Refresh access token</a>
</li>
<li class="tocify-item level-2" data-unique="authentication-POSTapi-auth-logout">
<a href="#authentication-POSTapi-auth-logout">Logout current session</a>
</li>
</ul>
</ul>
</div>
<ul class="toc-footer" id="toc-footer">
<li style="padding-bottom: 5px;"><a href="{{ route("scribe.postman") }}">View Postman collection</a></li>
<li style="padding-bottom: 5px;"><a href="{{ route("scribe.openapi") }}">View OpenAPI spec</a></li>
<li><a href="http://github.com/knuckleswtf/scribe">Documentation powered by Scribe </a></li>
</ul>
<ul class="toc-footer" id="last-updated">
<li>Last updated: February 18, 2026</li>
</ul>
</div>
<div class="page-wrapper">
<div class="dark-box"></div>
<div class="content">
<h1 id="introduction">Introduction</h1>
<p>Resource planning and capacity management API</p>
<aside>
<strong>Base URL</strong>: <code>http://localhost/api</code>
</aside>
<pre><code>Authenticate by sending `Authorization: Bearer {access_token}` on protected endpoints.
Access tokens are valid for 60 minutes. Use `/api/auth/refresh` with your refresh token to obtain a new access token and refresh token pair.</code></pre>
<h1 id="authenticating-requests">Authenticating requests</h1>
<p>To authenticate requests, include an <strong><code>Authorization</code></strong> header with the value <strong><code>"Bearer Bearer {token}"</code></strong>.</p>
<p>All authenticated endpoints are marked with a <code>requires authentication</code> badge in the documentation below.</p>
<p>Get tokens from <code>POST /api/auth/login</code>, send access token as <code>Bearer {token}</code>, and renew with <code>POST /api/auth/refresh</code> before access token expiry.</p>
<h1 id="authentication">Authentication</h1>
<p>Endpoints for JWT authentication and session lifecycle.</p>
<h2 id="authentication-POSTapi-auth-login">Login and get tokens</h2>
<p>
<small class="badge badge-darkred">requires authentication</small>
</p>
<p>Authenticate with email and password to receive an access token and refresh token.</p>
<span id="example-requests-POSTapi-auth-login">
<blockquote>Example request:</blockquote>
<div class="bash-example">
<pre><code class="language-bash">curl --request POST \
"http://localhost/api/api/auth/login" \
--header "Authorization: Bearer Bearer {token}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"user@example.com\",
\"password\": \"secret123\"
}"
</code></pre></div>
<div class="javascript-example">
<pre><code class="language-javascript">const url = new URL(
"http://localhost/api/api/auth/login"
);
const headers = {
"Authorization": "Bearer Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "user@example.com",
"password": "secret123"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response =&gt; response.json());</code></pre></div>
</span>
<span id="example-responses-POSTapi-auth-login">
<blockquote>
<p>Example response (200):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;access_token&quot;: &quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...&quot;,
&quot;refresh_token&quot;: &quot;abc123def456&quot;,
&quot;token_type&quot;: &quot;bearer&quot;,
&quot;expires_in&quot;: 3600,
&quot;user&quot;: {
&quot;id&quot;: &quot;550e8400-e29b-41d4-a716-446655440000&quot;,
&quot;name&quot;: &quot;Alice Johnson&quot;,
&quot;email&quot;: &quot;user@example.com&quot;,
&quot;role&quot;: &quot;manager&quot;
}
}</code>
</pre>
<blockquote>
<p>Example response (401):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;message&quot;: &quot;Invalid credentials&quot;
}</code>
</pre>
<blockquote>
<p>Example response (403):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;message&quot;: &quot;Account is inactive&quot;
}</code>
</pre>
<blockquote>
<p>Example response (422):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;errors&quot;: {
&quot;email&quot;: [
&quot;The email field is required.&quot;
],
&quot;password&quot;: [
&quot;The password field is required.&quot;
]
}
}</code>
</pre>
</span>
<span id="execution-results-POSTapi-auth-login" hidden>
<blockquote>Received response<span
id="execution-response-status-POSTapi-auth-login"></span>:
</blockquote>
<pre class="json"><code id="execution-response-content-POSTapi-auth-login"
data-empty-response-text="<Empty response>" style="max-height: 400px;"></code></pre>
</span>
<span id="execution-error-POSTapi-auth-login" hidden>
<blockquote>Request failed with error:</blockquote>
<pre><code id="execution-error-message-POSTapi-auth-login">
Tip: Check that you&#039;re properly connected to the network.
If you&#039;re a maintainer of ths API, verify that your API is running and you&#039;ve enabled CORS.
You can check the Dev Tools console for debugging information.</code></pre>
</span>
<form id="form-POSTapi-auth-login" data-method="POST"
data-path="api/auth/login"
data-authed="1"
data-hasfiles="0"
data-isarraybody="0"
autocomplete="off"
onsubmit="event.preventDefault(); executeTryOut('POSTapi-auth-login', this);">
<h3>
Request&nbsp;&nbsp;&nbsp;
<button type="button"
style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-tryout-POSTapi-auth-login"
onclick="tryItOut('POSTapi-auth-login');">Try it out
</button>
<button type="button"
style="background-color: #c97a7e; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-canceltryout-POSTapi-auth-login"
onclick="cancelTryOut('POSTapi-auth-login');" hidden>Cancel 🛑
</button>&nbsp;&nbsp;
<button type="submit"
style="background-color: #6ac174; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-executetryout-POSTapi-auth-login"
data-initial-text="Send Request 💥"
data-loading-text="⏱ Sending..."
hidden>Send Request 💥
</button>
</h3>
<p>
<small class="badge badge-black">POST</small>
<b><code>api/auth/login</code></b>
</p>
<h4 class="fancy-heading-panel"><b>Headers</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Authorization</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Authorization" class="auth-value" data-endpoint="POSTapi-auth-login"
value="Bearer Bearer {token}"
data-component="header">
<br>
<p>Example: <code>Bearer Bearer {token}</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Content-Type</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Content-Type" data-endpoint="POSTapi-auth-login"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Accept</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Accept" data-endpoint="POSTapi-auth-login"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<h4 class="fancy-heading-panel"><b>Body Parameters</b></h4>
<div style=" padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>email</code></b>&nbsp;&nbsp;
<small>string</small>&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="email" data-endpoint="POSTapi-auth-login"
value="user@example.com"
data-component="body">
<br>
<p>User email address. Example: <code>user@example.com</code></p>
</div>
<div style=" padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>password</code></b>&nbsp;&nbsp;
<small>string</small>&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="password" data-endpoint="POSTapi-auth-login"
value="secret123"
data-component="body">
<br>
<p>User password. Example: <code>secret123</code></p>
</div>
</form>
<h2 id="authentication-POSTapi-auth-refresh">Refresh access token</h2>
<p>
<small class="badge badge-darkred">requires authentication</small>
</p>
<p>Exchange a valid refresh token for a new access token and refresh token pair.</p>
<span id="example-requests-POSTapi-auth-refresh">
<blockquote>Example request:</blockquote>
<div class="bash-example">
<pre><code class="language-bash">curl --request POST \
"http://localhost/api/api/auth/refresh" \
--header "Authorization: Bearer Bearer {token}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"refresh_token\": \"abc123def456\"
}"
</code></pre></div>
<div class="javascript-example">
<pre><code class="language-javascript">const url = new URL(
"http://localhost/api/api/auth/refresh"
);
const headers = {
"Authorization": "Bearer Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"refresh_token": "abc123def456"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response =&gt; response.json());</code></pre></div>
</span>
<span id="example-responses-POSTapi-auth-refresh">
<blockquote>
<p>Example response (200):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;access_token&quot;: &quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...&quot;,
&quot;refresh_token&quot;: &quot;newtoken123&quot;,
&quot;token_type&quot;: &quot;bearer&quot;,
&quot;expires_in&quot;: 3600
}</code>
</pre>
<blockquote>
<p>Example response (401):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;message&quot;: &quot;Invalid or expired refresh token&quot;
}</code>
</pre>
</span>
<span id="execution-results-POSTapi-auth-refresh" hidden>
<blockquote>Received response<span
id="execution-response-status-POSTapi-auth-refresh"></span>:
</blockquote>
<pre class="json"><code id="execution-response-content-POSTapi-auth-refresh"
data-empty-response-text="<Empty response>" style="max-height: 400px;"></code></pre>
</span>
<span id="execution-error-POSTapi-auth-refresh" hidden>
<blockquote>Request failed with error:</blockquote>
<pre><code id="execution-error-message-POSTapi-auth-refresh">
Tip: Check that you&#039;re properly connected to the network.
If you&#039;re a maintainer of ths API, verify that your API is running and you&#039;ve enabled CORS.
You can check the Dev Tools console for debugging information.</code></pre>
</span>
<form id="form-POSTapi-auth-refresh" data-method="POST"
data-path="api/auth/refresh"
data-authed="1"
data-hasfiles="0"
data-isarraybody="0"
autocomplete="off"
onsubmit="event.preventDefault(); executeTryOut('POSTapi-auth-refresh', this);">
<h3>
Request&nbsp;&nbsp;&nbsp;
<button type="button"
style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-tryout-POSTapi-auth-refresh"
onclick="tryItOut('POSTapi-auth-refresh');">Try it out
</button>
<button type="button"
style="background-color: #c97a7e; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-canceltryout-POSTapi-auth-refresh"
onclick="cancelTryOut('POSTapi-auth-refresh');" hidden>Cancel 🛑
</button>&nbsp;&nbsp;
<button type="submit"
style="background-color: #6ac174; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-executetryout-POSTapi-auth-refresh"
data-initial-text="Send Request 💥"
data-loading-text="⏱ Sending..."
hidden>Send Request 💥
</button>
</h3>
<p>
<small class="badge badge-black">POST</small>
<b><code>api/auth/refresh</code></b>
</p>
<h4 class="fancy-heading-panel"><b>Headers</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Authorization</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Authorization" class="auth-value" data-endpoint="POSTapi-auth-refresh"
value="Bearer Bearer {token}"
data-component="header">
<br>
<p>Example: <code>Bearer Bearer {token}</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Content-Type</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Content-Type" data-endpoint="POSTapi-auth-refresh"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Accept</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Accept" data-endpoint="POSTapi-auth-refresh"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<h4 class="fancy-heading-panel"><b>Body Parameters</b></h4>
<div style=" padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>refresh_token</code></b>&nbsp;&nbsp;
<small>string</small>&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="refresh_token" data-endpoint="POSTapi-auth-refresh"
value="abc123def456"
data-component="body">
<br>
<p>Refresh token returned by login. Example: <code>abc123def456</code></p>
</div>
</form>
<h2 id="authentication-POSTapi-auth-logout">Logout current session</h2>
<p>
<small class="badge badge-darkred">requires authentication</small>
</p>
<p>Invalidate a refresh token and end the active authenticated session.</p>
<span id="example-requests-POSTapi-auth-logout">
<blockquote>Example request:</blockquote>
<div class="bash-example">
<pre><code class="language-bash">curl --request POST \
"http://localhost/api/api/auth/logout" \
--header "Authorization: Bearer Bearer {token}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"refresh_token\": \"abc123def456\"
}"
</code></pre></div>
<div class="javascript-example">
<pre><code class="language-javascript">const url = new URL(
"http://localhost/api/api/auth/logout"
);
const headers = {
"Authorization": "Bearer Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"refresh_token": "abc123def456"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response =&gt; response.json());</code></pre></div>
</span>
<span id="example-responses-POSTapi-auth-logout">
<blockquote>
<p>Example response (200):</p>
</blockquote>
<pre>
<code class="language-json" style="max-height: 300px;">{
&quot;message&quot;: &quot;Logged out successfully&quot;
}</code>
</pre>
</span>
<span id="execution-results-POSTapi-auth-logout" hidden>
<blockquote>Received response<span
id="execution-response-status-POSTapi-auth-logout"></span>:
</blockquote>
<pre class="json"><code id="execution-response-content-POSTapi-auth-logout"
data-empty-response-text="<Empty response>" style="max-height: 400px;"></code></pre>
</span>
<span id="execution-error-POSTapi-auth-logout" hidden>
<blockquote>Request failed with error:</blockquote>
<pre><code id="execution-error-message-POSTapi-auth-logout">
Tip: Check that you&#039;re properly connected to the network.
If you&#039;re a maintainer of ths API, verify that your API is running and you&#039;ve enabled CORS.
You can check the Dev Tools console for debugging information.</code></pre>
</span>
<form id="form-POSTapi-auth-logout" data-method="POST"
data-path="api/auth/logout"
data-authed="1"
data-hasfiles="0"
data-isarraybody="0"
autocomplete="off"
onsubmit="event.preventDefault(); executeTryOut('POSTapi-auth-logout', this);">
<h3>
Request&nbsp;&nbsp;&nbsp;
<button type="button"
style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-tryout-POSTapi-auth-logout"
onclick="tryItOut('POSTapi-auth-logout');">Try it out
</button>
<button type="button"
style="background-color: #c97a7e; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-canceltryout-POSTapi-auth-logout"
onclick="cancelTryOut('POSTapi-auth-logout');" hidden>Cancel 🛑
</button>&nbsp;&nbsp;
<button type="submit"
style="background-color: #6ac174; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-executetryout-POSTapi-auth-logout"
data-initial-text="Send Request 💥"
data-loading-text="⏱ Sending..."
hidden>Send Request 💥
</button>
</h3>
<p>
<small class="badge badge-black">POST</small>
<b><code>api/auth/logout</code></b>
</p>
<h4 class="fancy-heading-panel"><b>Headers</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Authorization</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Authorization" class="auth-value" data-endpoint="POSTapi-auth-logout"
value="Bearer Bearer {token}"
data-component="header">
<br>
<p>Example: <code>Bearer Bearer {token}</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Content-Type</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Content-Type" data-endpoint="POSTapi-auth-logout"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Accept</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Accept" data-endpoint="POSTapi-auth-logout"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<h4 class="fancy-heading-panel"><b>Body Parameters</b></h4>
<div style=" padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>refresh_token</code></b>&nbsp;&nbsp;
<small>string</small>&nbsp;
<i>optional</i> &nbsp;
&nbsp;
<input type="text" style="display: none"
name="refresh_token" data-endpoint="POSTapi-auth-logout"
value="abc123def456"
data-component="body">
<br>
<p>Optional refresh token to invalidate immediately. Example: <code>abc123def456</code></p>
</div>
</form>
</div>
<div class="dark-box">
<div class="lang-selector">
<button type="button" class="lang-button" data-language-name="bash">bash</button>
<button type="button" class="lang-button" data-language-name="javascript">javascript</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -93,14 +93,9 @@ public function index(Request $request): JsonResponse
### Group Organization
| Group | Controller | Endpoints |
|----------------|-----------------------|-----------|
| Authentication | AuthController | 5 |
| Team Members | TeamMemberController | 5 |
| Projects | ProjectController | 8 |
| Allocations | AllocationController | 6 |
| Actuals | ActualController | 5 |
| Capacity | CapacityController | 6 |
| Reports | ReportController | 5 |
| Master Data | MasterDataController | 9 |
| Authentication | AuthController | 3 |
Additional groups are intentionally deferred until corresponding controllers are added to this repository.
### Authentication Documentation
Include a dedicated section explaining:
@@ -113,15 +108,8 @@ Include a dedicated section explaining:
1. Configure Scribe (`config/scribe.php`)
2. Annotate AuthController (most critical, used by all)
3. Annotate TeamMemberController
4. Annotate ProjectController
5. Annotate AllocationController
6. Annotate ActualController
7. Annotate CapacityController
8. Annotate ReportController
9. Annotate MasterDataController
10. Generate documentation (`php artisan scribe:generate`)
11. Verify SwaggerUI at `/api/documentation`
3. Generate documentation (`php artisan scribe:generate`)
4. Verify SwaggerUI at `/api/documentation`
## File Changes
@@ -130,13 +118,7 @@ Include a dedicated section explaining:
### Modified Files
- `backend/app/Http/Controllers/Api/AuthController.php`
- `backend/app/Http/Controllers/Api/TeamMemberController.php`
- `backend/app/Http/Controllers/Api/ProjectController.php`
- `backend/app/Http/Controllers/Api/AllocationController.php`
- `backend/app/Http/Controllers/Api/ActualController.php`
- `backend/app/Http/Controllers/Api/CapacityController.php`
- `backend/app/Http/Controllers/Api/ReportController.php`
- `backend/app/Http/Controllers/Api/MasterDataController.php`
- `backend/config/cors.php`
## Testing
- Run `php artisan scribe:generate` - must complete without errors

View File

@@ -4,7 +4,7 @@
Add comprehensive API documentation annotations to all Laravel controllers using Laravel Scribe. This enables auto-generated SwaggerUI documentation accessible at `/api/documentation`.
## Goals
- Annotate ALL existing API controllers with Scribe annotations
- Annotate existing authentication API endpoints with Scribe annotations
- Generate browsable API documentation
- Ensure documentation stays in sync with implementation
- Enable frontend developers to reference accurate API specs
@@ -21,13 +21,8 @@ Add comprehensive API documentation annotations to all Laravel controllers using
### Controllers to Document
1. **AuthController** - Login, logout, token refresh endpoints
2. **TeamMemberController** - CRUD for team members
3. **ProjectController** - CRUD, status transitions, estimates
4. **AllocationController** - CRUD, bulk operations, matrix view
5. **ActualController** - CRUD, logging hours
6. **CapacityController** - Capacity calculations, holidays, PTO
7. **ReportController** - Forecast, utilization, costs, variance reports
8. **MasterDataController** - Roles, statuses, types management
Other controller groups in the broader roadmap are deferred until those controllers/routes are present in this repository.
### Annotations Required
Each endpoint must have:
@@ -38,10 +33,10 @@ Each endpoint must have:
- `@response 401|403|422` - Error responses
## Success Criteria
- [ ] All controllers have Scribe annotations
- [ ] AuthController endpoints have Scribe annotations
- [ ] `php artisan scribe:generate` runs without errors
- [ ] SwaggerUI accessible at `/api/documentation`
- [ ] All endpoints documented with request/response examples
- [ ] Authentication endpoints documented with request/response examples
- [ ] Authentication section explains JWT flow
## Estimated Effort

View File

@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Generate Authentication API Documentation
The system SHALL generate API documentation for authentication endpoints using Laravel Scribe.
#### Scenario: Generate docs successfully
- **WHEN** `php artisan scribe:generate` is run
- **THEN** the command completes without errors
- **AND** generated documentation includes `POST /api/auth/login`, `POST /api/auth/refresh`, and `POST /api/auth/logout`
### Requirement: Serve Documentation UI
The system SHALL serve documentation at `/api/documentation`.
#### Scenario: Access generated docs
- **WHEN** a client requests `GET /api/documentation`
- **THEN** the application responds with HTTP 200
- **AND** the page contains the configured API title and authentication guidance

View File

@@ -0,0 +1,32 @@
# Tasks: API Documentation with Scribe
## Phase 1: Configure Scribe
- [x] 0.1 Install Scribe (if not already installed): `composer require knuckleswtf/scribe`
- [x] 0.2 Publish Scribe config: `php artisan vendor:publish --tag=scribe-config`
- [x] 0.3 Configure `config/scribe.php` with Headroom settings
- [x] 0.4 Add `/api/documentation` to CORS allowed paths
## Phase 2: Annotate Controllers
### AuthController
- [x] 0.5 Add `@group Authentication` to class
- [x] 0.6 Document `POST /api/auth/login` with @bodyParam, @response
- [x] 0.7 Document `POST /api/auth/refresh` with @authenticated, @response
- [x] 0.8 Document `POST /api/auth/logout` with @authenticated, @response
- [x] 0.9 Add authentication section to Scribe config
## Phase 3: Generate & Verify
- [x] 0.54 Run `php artisan scribe:generate`
- [x] 0.55 Verify no errors in generation
- [x] 0.56 Access `/api/documentation` in browser
- [x] 0.57 Verify all endpoints appear in documentation
- [x] 0.58 Test "Try it out" for login endpoint
- [x] 0.59 Verify authentication flow is documented
## Commits
1. `chore(docs): Configure Laravel Scribe for API documentation`
2. `docs(api): Add Scribe annotations to AuthController`
3. `docs(api): Generate and verify SwaggerUI documentation`

View File

@@ -1,94 +0,0 @@
# Tasks: API Documentation with Scribe
## Phase 1: Configure Scribe
- [ ] 0.1 Install Scribe (if not already installed): `composer require knuckleswtf/scribe`
- [ ] 0.2 Publish Scribe config: `php artisan vendor:publish --tag=scribe-config`
- [ ] 0.3 Configure `config/scribe.php` with Headroom settings
- [ ] 0.4 Add `/api/documentation` to CORS allowed paths
## Phase 2: Annotate Controllers
### AuthController
- [ ] 0.5 Add `@group Authentication` to class
- [ ] 0.6 Document `POST /api/auth/login` with @bodyParam, @response
- [ ] 0.7 Document `POST /api/auth/refresh` with @authenticated, @response
- [ ] 0.8 Document `POST /api/auth/logout` with @authenticated, @response
- [ ] 0.9 Add authentication section to Scribe config
### TeamMemberController
- [ ] 0.10 Add `@group Team Members` to class
- [ ] 0.11 Document `GET /api/team-members` with @queryParam filters
- [ ] 0.12 Document `POST /api/team-members` with @bodyParam
- [ ] 0.13 Document `GET /api/team-members/{id}`
- [ ] 0.14 Document `PUT /api/team-members/{id}`
- [ ] 0.15 Document `DELETE /api/team-members/{id}`
### ProjectController
- [ ] 0.16 Add `@group Projects` to class
- [ ] 0.17 Document `GET /api/projects` with @queryParam filters
- [ ] 0.18 Document `POST /api/projects` with @bodyParam
- [ ] 0.19 Document `GET /api/projects/{id}`
- [ ] 0.20 Document `PUT /api/projects/{id}`
- [ ] 0.21 Document `PUT /api/projects/{id}/status`
- [ ] 0.22 Document `PUT /api/projects/{id}/estimate`
- [ ] 0.23 Document `PUT /api/projects/{id}/forecast`
### AllocationController
- [ ] 0.24 Add `@group Allocations` to class
- [ ] 0.25 Document `GET /api/allocations` (matrix view)
- [ ] 0.26 Document `POST /api/allocations`
- [ ] 0.27 Document `PUT /api/allocations/{id}`
- [ ] 0.28 Document `DELETE /api/allocations/{id}`
- [ ] 0.29 Document `POST /api/allocations/bulk`
### ActualController
- [ ] 0.30 Add `@group Actuals` to class
- [ ] 0.31 Document `GET /api/actuals`
- [ ] 0.32 Document `POST /api/actuals`
- [ ] 0.33 Document `PUT /api/actuals/{id}`
- [ ] 0.34 Document validation rules (future month rejection)
### CapacityController
- [ ] 0.35 Add `@group Capacity` to class
- [ ] 0.36 Document `GET /api/capacity`
- [ ] 0.37 Document `GET /api/capacity/team`
- [ ] 0.38 Document `GET /api/capacity/revenue`
- [ ] 0.39 Document `POST /api/holidays`
- [ ] 0.40 Document `POST /api/ptos`
### ReportController
- [ ] 0.41 Add `@group Reports` to class
- [ ] 0.42 Document `GET /api/reports/forecast`
- [ ] 0.43 Document `GET /api/reports/utilization`
- [ ] 0.44 Document `GET /api/reports/costs`
- [ ] 0.45 Document `GET /api/reports/variance`
- [ ] 0.46 Document `GET /api/reports/allocation`
### MasterDataController
- [ ] 0.47 Add `@group Master Data` to class
- [ ] 0.48 Document `GET /api/roles`
- [ ] 0.49 Document `POST /api/roles`
- [ ] 0.50 Document `PUT /api/roles/{id}`
- [ ] 0.51 Document `DELETE /api/roles/{id}`
- [ ] 0.52 Document project-statuses endpoints
- [ ] 0.53 Document project-types endpoints
## Phase 3: Generate & Verify
- [ ] 0.54 Run `php artisan scribe:generate`
- [ ] 0.55 Verify no errors in generation
- [ ] 0.56 Access `/api/documentation` in browser
- [ ] 0.57 Verify all endpoints appear in documentation
- [ ] 0.58 Test "Try it out" for login endpoint
- [ ] 0.59 Verify authentication flow is documented
## Commits
1. `chore(docs): Configure Laravel Scribe for API documentation`
2. `docs(api): Add Scribe annotations to AuthController`
3. `docs(api): Add Scribe annotations to TeamMemberController`
4. `docs(api): Add Scribe annotations to ProjectController`
5. `docs(api): Add Scribe annotations to AllocationController`
6. `docs(api): Add Scribe annotations to remaining controllers`
7. `docs(api): Generate and verify SwaggerUI documentation`

View File

@@ -0,0 +1,22 @@
# Capability: API Documentation
## Purpose
Provide generated, accessible API documentation for currently implemented authentication endpoints.
## Requirements
### Requirement: Generate Authentication API Documentation
The system SHALL generate API documentation for authentication endpoints using Laravel Scribe.
#### Scenario: Generate docs successfully
- **WHEN** `php artisan scribe:generate` is run
- **THEN** the command completes without errors
- **AND** generated documentation includes `POST /api/auth/login`, `POST /api/auth/refresh`, and `POST /api/auth/logout`
### Requirement: Serve Documentation UI
The system SHALL serve documentation at `/api/documentation`.
#### Scenario: Access generated docs
- **WHEN** a client requests `GET /api/documentation`
- **THEN** the application responds with HTTP 200
- **AND** the page contains the configured API title and authentication guidance