diff --git a/backend/.scribe/.filehashes b/backend/.scribe/.filehashes
new file mode 100644
index 00000000..9cf1036f
--- /dev/null
+++ b/backend/.scribe/.filehashes
@@ -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
\ No newline at end of file
diff --git a/backend/.scribe/auth.md b/backend/.scribe/auth.md
new file mode 100644
index 00000000..bf1aa6a7
--- /dev/null
+++ b/backend/.scribe/auth.md
@@ -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.
diff --git a/backend/.scribe/endpoints.cache/00.yaml b/backend/.scribe/endpoints.cache/00.yaml
new file mode 100644
index 00000000..97014a68
--- /dev/null
+++ b/backend/.scribe/endpoints.cache/00.yaml
@@ -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
diff --git a/backend/.scribe/endpoints/00.yaml b/backend/.scribe/endpoints/00.yaml
new file mode 100644
index 00000000..f4794ee7
--- /dev/null
+++ b/backend/.scribe/endpoints/00.yaml
@@ -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
diff --git a/backend/.scribe/endpoints/custom.0.yaml b/backend/.scribe/endpoints/custom.0.yaml
new file mode 100644
index 00000000..4b023521
--- /dev/null
+++ b/backend/.scribe/endpoints/custom.0.yaml
@@ -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
diff --git a/backend/.scribe/intro.md b/backend/.scribe/intro.md
new file mode 100644
index 00000000..bc297e8a
--- /dev/null
+++ b/backend/.scribe/intro.md
@@ -0,0 +1,12 @@
+# Introduction
+
+Resource planning and capacity management API
+
+
+ Base URL : http://localhost/api
+
+
+ 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.
+
diff --git a/backend/composer.json b/backend/composer.json
index 0a9b8501..456661eb 100644
--- a/backend/composer.json
+++ b/backend/composer.json
@@ -10,6 +10,7 @@
"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"
},
diff --git a/backend/composer.lock b/backend/composer.lock
index 92ffa71a..b623cc3a 100644
--- a/backend/composer.lock
+++ b/backend/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": "2782b93777d132b40f4d1f45c0483384",
+ "content-hash": "eb1f270f832bd2bd086e4cccb3a4945d",
"packages": [
{
"name": "brick/math",
@@ -3043,24 +3043,24 @@
},
{
"name": "parsedown/parsedown",
- "version": "1.8.0",
+ "version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/parsedown/parsedown.git",
- "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7"
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/parsedown/parsedown/zipball/96baaad00f71ba04d76e45b4620f54d3beabd6f7",
- "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7",
+ "url": "https://api.github.com/repos/parsedown/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
- "php": ">=7.1"
+ "php": ">=5.3.0"
},
"require-dev": {
- "phpunit/phpunit": "^7.5|^8.5|^9.6"
+ "phpunit/phpunit": "^4.8.35"
},
"type": "library",
"autoload": {
@@ -3087,9 +3087,9 @@
],
"support": {
"issues": "https://github.com/parsedown/parsedown/issues",
- "source": "https://github.com/parsedown/parsedown/tree/1.8.0"
+ "source": "https://github.com/parsedown/parsedown/tree/1.7.4"
},
- "time": "2026-02-16T11:41:01+00:00"
+ "time": "2019-12-30T22:54:17+00:00"
},
{
"name": "phpoption/phpoption",
diff --git a/backend/config/scribe.php b/backend/config/scribe.php
index 487f86f6..3fb82ec7 100644
--- a/backend/config/scribe.php
+++ b/backend/config/scribe.php
@@ -4,7 +4,6 @@ use Knuckles\Scribe\Config\AuthIn;
use Knuckles\Scribe\Config\Defaults;
use Knuckles\Scribe\Extracting\Strategies;
-use function Knuckles\Scribe\Config\configureStrategy;
use function Knuckles\Scribe\Config\removeStrategies;
// Only the most common configs are shown. See the https://scribe.knuckles.wtf/laravel/reference/config for all.
@@ -45,7 +44,7 @@ return [
// Exclude these routes even if they matched the rules above.
'exclude' => [
- // 'GET /health', 'admin.*'
+ 'api/user',
],
],
],
@@ -231,15 +230,9 @@ return [
'bodyParameters' => [
...Defaults::BODY_PARAMETERS_STRATEGIES,
],
- 'responses' => configureStrategy(
+ 'responses' => removeStrategies(
Defaults::RESPONSES_STRATEGIES,
- Strategies\Responses\ResponseCalls::withSettings(
- only: ['GET *'],
- // Recommended: disable debug mode in response calls to avoid error stack traces in responses
- config: [
- 'app.debug' => false,
- ]
- )
+ [Strategies\Responses\ResponseCalls::class],
),
'responseFields' => [
...Defaults::RESPONSE_FIELDS_STRATEGIES,
diff --git a/backend/public/vendor/scribe/css/theme-default.print.css b/backend/public/vendor/scribe/css/theme-default.print.css
new file mode 100644
index 00000000..18ab760e
--- /dev/null
+++ b/backend/public/vendor/scribe/css/theme-default.print.css
@@ -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
+}
diff --git a/backend/public/vendor/scribe/css/theme-default.style.css b/backend/public/vendor/scribe/css/theme-default.style.css
new file mode 100644
index 00000000..08f2e93a
--- /dev/null
+++ b/backend/public/vendor/scribe/css/theme-default.style.css
@@ -0,0 +1,1094 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+html {
+ font-family: 'Open Sans', sans-serif;
+ font-size: 1.2em;
+ -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 {
+ display: block
+}
+
+summary {
+ cursor: pointer;
+}
+
+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: 700
+}
+
+dfn {
+ font-style: italic
+}
+
+h1 {
+ font-size: 2em;
+ margin: .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: -.5em
+}
+
+sub {
+ bottom: -.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 silver;
+ margin: 0 2px;
+ padding: .35em .625em .75em
+}
+
+legend {
+ border: 0;
+ padding: 0
+}
+
+textarea {
+ overflow: auto
+}
+
+optgroup {
+ font-weight: 700
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0
+}
+
+td,
+th {
+ padding: 0
+}
+
+body,
+html {
+ font-family: 'Open Sans', Helvetica Neue, Helvetica, Arial, Microsoft Yahei, 微软雅黑, STXihei, 华文细黑, sans-serif;
+ font-size: 16px;
+}
+
+.content h1,
+.content h2,
+.content h3,
+.content h4,
+.content h5,
+.content h6 {
+ font-family: 'Open Sans', Helvetica Neue, Helvetica, Arial, Microsoft Yahei, 微软雅黑, STXihei, 华文细黑, sans-serif;
+}
+
+.content h1,
+.content h2,
+.content h3,
+.content h4,
+.content h5,
+.content h6 {
+ font-weight: 700
+}
+
+.content code,
+.content pre {
+ font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
+ font-size: 14px;
+ line-height: 1.5
+}
+
+.content code {
+ word-break: break-all;
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -ms-hyphens: auto;
+ hyphens: auto
+}
+
+.content aside.notice:before,
+.content aside.success:before,
+.content aside.warning:before,
+.tocify-wrapper>.search:before {
+ font-family: 'Open Sans', sans-serif;
+ speak: none;
+ font-style: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1
+}
+
+.content aside.warning:before {
+ content: "✋"
+}
+
+.content aside.notice:before {
+ content: "ℹ"
+}
+
+.content aside.success:before {
+ content: "✅"
+}
+
+.tocify-wrapper>.search:before {
+ content: "🔎"
+}
+
+.highlight .c,
+.highlight .c1,
+.highlight .cm,
+.highlight .cs {
+ color: #909090
+}
+
+.highlight,
+.highlight .w {
+ background-color: #292929
+}
+
+.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: .5em;
+ background: #23241f
+}
+
+.hljs,
+.hljs-subst,
+.hljs-tag {
+ color: #f8f8f2
+}
+
+.hljs-emphasis,
+.hljs-strong {
+ color: #a8a8a2
+}
+
+.hljs-bullet,
+.hljs-link,
+.hljs-literal,
+.hljs-number,
+.hljs-quote,
+.hljs-regexp {
+ color: #ae81ff
+}
+
+.hljs-code,
+.hljs-section,
+.hljs-selector-class,
+.hljs-title {
+ color: #a6e22e
+}
+
+.hljs-strong {
+ font-weight: 700
+}
+
+.hljs-emphasis {
+ font-style: italic
+}
+
+.hljs-attr,
+.hljs-keyword,
+.hljs-name,
+.hljs-selector-tag {
+ color: #f92672
+}
+
+.hljs-attribute,
+.hljs-symbol {
+ color: #66d9ef
+}
+
+.hljs-class .hljs-title,
+.hljs-params {
+ color: #f8f8f2
+}
+
+.hljs-addition,
+.hljs-built_in,
+.hljs-builtin-name,
+.hljs-selector-attr,
+.hljs-selector-id,
+.hljs-selector-pseudo,
+.hljs-string,
+.hljs-template-variable,
+.hljs-type,
+.hljs-variable {
+ color: #e6db74
+}
+
+.hljs-comment,
+.hljs-deletion,
+.hljs-meta {
+ color: #75715e
+}
+
+body,
+html {
+ color: #333;
+ padding: 0;
+ margin: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: whitesmoke;
+ height: 100%;
+ -webkit-text-size-adjust: none
+}
+
+#toc>ul>li>a>span {
+ float: right;
+ background-color: #2484ff;
+ border-radius: 40px;
+ width: 20px
+}
+
+.tocify-wrapper {
+ transition: left .3s ease-in-out;
+ overflow-y: auto;
+ overflow-x: hidden;
+ position: fixed;
+ z-index: 30;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 230px;
+ background-color: #393939;
+ font-size: 13px;
+ font-weight: 700
+}
+
+.tocify-wrapper .lang-selector {
+ display: none
+}
+
+.tocify-wrapper .lang-selector a {
+ padding-top: .5em;
+ padding-bottom: .5em
+}
+
+.tocify-wrapper>img {
+ display: block
+}
+
+.tocify-wrapper>.search {
+ position: relative
+}
+
+.tocify-wrapper>.search input {
+ background: #393939;
+ border-width: 0 0 1px;
+ border-color: #666;
+ padding: 6px 0 6px 20px;
+ box-sizing: border-box;
+ margin: 10px 15px;
+ width: 200px;
+ outline: none;
+ color: #fff;
+ border-radius: 0
+}
+
+.tocify-wrapper>.search:before {
+ position: absolute;
+ top: 17px;
+ left: 15px;
+ color: #fff
+}
+
+.tocify-wrapper img+.tocify {
+ margin-top: 20px
+}
+
+.tocify-wrapper .search-results {
+ margin-top: 0;
+ box-sizing: border-box;
+ height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ transition-property: height, margin;
+ transition-duration: .18s;
+ transition-timing-function: ease-in-out;
+ background: linear-gradient(180deg, rgba(0, 0, 0, .2), transparent 8px), linear-gradient(0deg, rgba(0, 0, 0, .2), transparent 8px), linear-gradient(180deg, #000, transparent 1.5px), linear-gradient(0deg, #939393, hsla(0, 0%, 58%, 0) 1.5px), #262626
+}
+
+.tocify-wrapper .search-results.visible {
+ height: 30%;
+ margin-bottom: 1em
+}
+
+.tocify-wrapper .search-results li {
+ margin: 1em 15px;
+ line-height: 1
+}
+
+.tocify-wrapper a {
+ color: #fff;
+ text-decoration: none
+}
+
+.tocify-wrapper .search-results a:hover {
+ text-decoration: underline
+}
+
+.tocify-wrapper .toc-footer li,
+.tocify-wrapper .tocify-item>a {
+ padding: 0 15px;
+ display: block;
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis
+}
+.tocify-wrapper .tocify-item.level-3>a {
+ padding: 0 25px;
+}
+
+.tocify-wrapper li,
+.tocify-wrapper ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ line-height: 28px
+}
+
+.tocify-wrapper li {
+ color: #fff;
+ transition-property: background;
+ transition-timing-function: linear;
+ transition-duration: .23s
+}
+
+.tocify-wrapper .tocify-focus {
+ box-shadow: 0 1px 0 #000;
+ background-color: #2467af;
+ color: #fff;
+ font-weight: bold;
+}
+
+.tocify-wrapper .tocify-subheader {
+ display: none;
+ background-color: #262626;
+ font-weight: 500;
+ background: linear-gradient(180deg, rgba(0, 0, 0, .2), transparent 8px), linear-gradient(0deg, rgba(0, 0, 0, .2), transparent 8px), linear-gradient(180deg, #000, transparent 1.5px), linear-gradient(0deg, #939393, hsla(0, 0%, 58%, 0) 1.5px), #262626
+}
+
+.tocify-wrapper .jets-searching .tocify-subheader,
+.tocify-wrapper .tocify-subheader.visible {
+ display: block;
+}
+
+.tocify-wrapper .tocify-subheader .tocify-item>a {
+ padding-left: 25px;
+ font-size: 12px
+}
+
+.tocify-wrapper .tocify-subheader .tocify-item.level-3>a {
+ padding-left: 35px;
+}
+
+.tocify-wrapper .tocify-subheader>li:last-child {
+ box-shadow: none
+}
+
+.tocify-wrapper .toc-footer {
+ padding: 1em 0;
+ margin-top: 1em;
+ border-top: 1px dashed #666
+}
+
+.tocify-wrapper .toc-footer a,
+.tocify-wrapper .toc-footer li {
+ color: #fff;
+ text-decoration: none
+}
+
+.tocify-wrapper .toc-footer a:hover {
+ text-decoration: underline
+}
+
+.tocify-wrapper .toc-footer li {
+ font-size: .8em;
+ line-height: 1.7;
+ text-decoration: none
+}
+
+#nav-button {
+ padding: 0 1.5em 5em 0;
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ color: #000;
+ text-decoration: none;
+ font-weight: 700;
+ opacity: .7;
+ line-height: 16px;
+ transition: left .3s ease-in-out
+}
+
+#nav-button span {
+ display: block;
+ padding: 6px;
+ background-color: rgba(234, 242, 246, .7);
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
+ -webkit-transform: rotate(-90deg) translate(-100%);
+ transform: rotate(-90deg) translate(-100%);
+ border-radius: 0 0 0 5px
+}
+
+#nav-button img {
+ height: 16px;
+ vertical-align: bottom
+}
+
+#nav-button:hover {
+ opacity: 1
+}
+
+#nav-button.open {
+ left: 230px
+}
+
+.page-wrapper {
+ margin-left: 230px;
+ position: relative;
+ z-index: 10;
+ background-color: #eaf2f6;
+ min-height: 100%;
+ padding-bottom: 1px
+}
+
+.page-wrapper .dark-box {
+ width: 50%;
+ background-color: #393939;
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0
+}
+
+.page-wrapper .lang-selector {
+ position: fixed;
+ z-index: 50;
+ border-bottom: 5px solid #393939
+}
+
+.lang-selector {
+ background-color: #222;
+ width: 100%;
+ font-weight: 700
+}
+
+.lang-selector button {
+ display: block;
+ float: left;
+ color: #fff;
+ text-decoration: none;
+ padding: 0 10px;
+ line-height: 30px;
+ outline: 0;
+ background: transparent;
+ border: none;
+}
+
+.lang-selector button:active,
+.lang-selector button:hover,
+.lang-selector button:focus {
+ background-color: #111;
+ color: #fff
+}
+
+.lang-selector button.active {
+ background-color: #393939;
+ color: #fff
+}
+
+.lang-selector:after {
+ content: '';
+ clear: both;
+ display: block
+}
+
+.content {
+ position: relative;
+ z-index: 30
+}
+
+.content:after {
+ content: '';
+ display: block;
+ clear: both
+}
+
+.content>aside,
+.content>details,
+.content>dl,
+.content>h1,
+.content>h2,
+.content>h3,
+.content>h4,
+.content>h5,
+.content>h6,
+.content>ol,
+.content>p,
+.content>table,
+.content>ul,
+.content>div,
+.content>form>aside,
+.content>form>details,
+.content>form>h1,
+.content>form>h2,
+.content>form>h3,
+.content>form>h4,
+.content>form>h5,
+.content>form>h6,
+.content>form>p,
+.content>form>table,
+.content>form>ul,
+.content>form>div {
+ margin-right: 50%;
+ padding: 0 28px;
+ box-sizing: border-box;
+ display: block;
+ text-shadow: 0 1px 0 #fff
+}
+
+.content>ol,
+.content>ul {
+ padding-left: 43px
+}
+
+.content>div,
+.content>h1,
+.content>h2 {
+ clear: both
+}
+
+.content h1 {
+ font-size: 30px;
+ padding-top: .5em;
+ padding-bottom: .5em;
+ border-bottom: 1px solid #ccc;
+ margin-bottom: 21px;
+ margin-top: 2em;
+ border-top: 1px solid #ddd;
+ background-image: linear-gradient(180deg, #fff, #f9f9f9)
+}
+
+.content div:first-child+h1,
+.content h1:first-child {
+ border-top-width: 0;
+ margin-top: 0
+}
+
+.content h2 {
+ font-size: 20px;
+ margin-top: 4em;
+ margin-bottom: 0;
+ border-top: 1px solid #ccc;
+ padding-top: 1.2em;
+ padding-bottom: 1.2em;
+ background-image: linear-gradient(180deg, hsla(0, 0%, 100%, .4), hsla(0, 0%, 100%, 0))
+}
+
+.content h1+div+h2,
+.content h1+h2 {
+ margin-top: -21px;
+ border-top: none
+}
+
+.content h3,
+.content h4,
+.content h5,
+.content h6 {
+ font-size: 15px;
+ margin-top: 2.5em;
+ margin-bottom: .8em
+}
+
+.content h4,
+.content h5,
+.content h6 {
+ font-size: 10px
+}
+
+.content hr {
+ margin: 2em 0;
+ border-top: 2px solid #393939;
+ border-bottom: 2px solid #eaf2f6
+}
+
+.content table {
+ margin-bottom: 1em;
+ overflow: auto
+}
+
+.content table td,
+.content table th {
+ text-align: left;
+ vertical-align: top;
+ line-height: 1.6
+}
+
+.content table th {
+ padding: 5px 10px;
+ border-bottom: 1px solid #ccc;
+ vertical-align: bottom
+}
+
+.content table td {
+ padding: 10px
+}
+
+.content table tr:last-child {
+ border-bottom: 1px solid #ccc
+}
+
+.content table tr:nth-child(odd)>td {
+ background-color: #ebf3f6
+}
+
+.content table tr:nth-child(even)>td {
+ background-color: #ebf2f6
+}
+
+.content dt {
+ font-weight: 700
+}
+
+.content dd {
+ margin-left: 15px
+}
+
+.content dd,
+.content dt,
+.content li,
+.content p {
+ line-height: 1.6;
+ margin-top: 0
+}
+
+.content img {
+ max-width: 100%
+}
+
+.content code {
+ padding: 3px;
+ border-radius: 3px
+}
+
+.content pre>code {
+ background-color: transparent;
+ padding: 0
+}
+
+.content aside {
+ padding-top: 1em;
+ padding-bottom: 1em;
+ margin-top: 1.5em;
+ margin-bottom: 1.5em;
+ background: #292929;
+ line-height: 1.6;
+ color: #c8c8c8;
+ text-shadow: none;
+}
+
+.content aside.info {
+ background: #8fbcd4;
+ text-shadow: 0 1px 0 #a0c6da;
+ color: initial;
+}
+
+.content aside.warning {
+ background-color: #c97a7e;
+ text-shadow: 0 1px 0 #d18e91;
+ color: initial;
+}
+
+.content aside.success {
+ background-color: #6ac174;
+ text-shadow: 0 1px 0 #80ca89;
+ color: initial;
+}
+
+.content aside:before {
+ vertical-align: middle;
+ padding-right: .5em;
+ font-size: 14px
+}
+
+.content .search-highlight {
+ padding: 2px;
+ margin: -2px;
+ border-radius: 4px;
+ border: 1px solid #f7e633;
+ text-shadow: 1px 1px 0 #666;
+ background: linear-gradient(to top left, #f7e633, #f1d32f)
+}
+
+.content blockquote,
+.content pre {
+ background-color: #292929;
+ color: #fff;
+ padding: 1.5em 28px;
+ margin: 0;
+ width: 50%;
+ float: right;
+ clear: right;
+ box-sizing: border-box;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, .4)
+}
+
+.content blockquote pre.sf-dump,
+.content pre pre.sf-dump {
+ width: 100%;
+}
+
+.content .annotation {
+ background-color: #292929;
+ color: #fff;
+ padding: 0 28px;
+ margin: 0;
+ width: 50%;
+ float: right;
+ clear: right;
+ box-sizing: border-box;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, .4)
+}
+
+.content .annotation pre {
+ padding: 0 0;
+ width: 100%;
+ float: none;
+}
+
+.content blockquote>p,
+.content pre>p {
+ margin: 0
+}
+
+.content blockquote a,
+.content pre a {
+ color: #fff;
+ text-decoration: none;
+ border-bottom: 1px dashed #ccc
+}
+
+.content blockquote>p {
+ background-color: #1c1c1c;
+ border-radius: 5px;
+ padding: 13px;
+ color: #ccc;
+ border-top: 1px solid #000;
+ border-bottom: 1px solid #404040
+}
+
+@media (max-width:930px) {
+ .tocify-wrapper {
+ left: -230px
+ }
+ .tocify-wrapper.open {
+ left: 0
+ }
+ .page-wrapper {
+ margin-left: 0
+ }
+ #nav-button {
+ display: block
+ }
+ .tocify-wrapper .tocify-item>a {
+ padding-top: .3em;
+ padding-bottom: .3em
+ }
+}
+
+@media (max-width:700px) {
+ .dark-box {
+ display: none
+ }
+ .tocify-wrapper .lang-selector {
+ display: block
+ }
+ .page-wrapper .lang-selector {
+ display: none
+ }
+ .content>aside,
+ .content>details,
+ .content>dl,
+ .content>h1,
+ .content>h2,
+ .content>h3,
+ .content>h4,
+ .content>h5,
+ .content>h6,
+ .content>ol,
+ .content>p,
+ .content>table,
+ .content>ul,
+ .content>div,
+ .content>form>aside,
+ .content>form>details,
+ .content>form>h1,
+ .content>form>h2,
+ .content>form>h3,
+ .content>form>h4,
+ .content>form>h5,
+ .content>form>h6,
+ .content>form>p,
+ .content>form>table,
+ .content>form>ul,
+ .content>form>div {
+ margin-right: 0;
+ }
+ .content blockquote,
+ .content pre {
+ float: none;
+ width: auto
+ }
+ .content .annotation {
+ float: none;
+ width: auto
+ }
+}
+
+.badge {
+ padding: 1px 9px 2px;
+ white-space: nowrap;
+ -webkit-border-radius: 9px;
+ -moz-border-radius: 9px;
+ border-radius: 9px;
+ color: #ffffff;
+ text-shadow: none !important;
+ font-weight: bold;
+}
+
+.badge.badge-darkred {
+ background-color: darkred;
+}
+
+.badge.badge-red {
+ background-color: red;
+}
+
+.badge.badge-blue {
+ background-color: blue;
+}
+
+.badge.badge-darkblue {
+ background-color: darkblue;
+}
+
+.badge.badge-green {
+ background-color: green;
+}
+
+.badge.badge-darkgoldenrod {
+ background-color: darkgoldenrod;
+}
+
+.badge.badge-darkgreen {
+ background-color: darkgreen;
+}
+
+.badge.badge-purple {
+ background-color: purple;
+}
+
+.badge.badge-black {
+ background-color: black;
+}
+
+.badge.badge-grey {
+ background-color: grey;
+}
+
+.fancy-heading-panel {
+ background-color: lightgrey;
+ border-radius: 5px;
+ padding-left: 5px !important;
+ padding-top: 5px !important;
+ padding-bottom: 5px !important;
+ margin-left: 25px;
+ margin-right: 10px;
+ width: 47%;
+}
+
+@media screen and (max-width: 700px) {
+ .fancy-heading-panel {
+ width: 95%;
+ }
+
+}
+
+button {
+ border: none;
+}
+
+* {
+ /* Foreground, Background */
+ scrollbar-color: #3c4c67 transparent;
+}
+*::-webkit-scrollbar { /* Background */
+ width: 10px;
+ height: 10px;
+ background: transparent;
+}
+
+*::-webkit-scrollbar-thumb { /* Foreground */
+ background: #626161;
+}
diff --git a/backend/public/vendor/scribe/images/navbar.png b/backend/public/vendor/scribe/images/navbar.png
new file mode 100644
index 00000000..df38e90d
Binary files /dev/null and b/backend/public/vendor/scribe/images/navbar.png differ
diff --git a/backend/public/vendor/scribe/js/theme-default-5.7.0.js b/backend/public/vendor/scribe/js/theme-default-5.7.0.js
new file mode 100644
index 00000000..31c84514
--- /dev/null
+++ b/backend/public/vendor/scribe/js/theme-default-5.7.0.js
@@ -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();
+});
diff --git a/backend/public/vendor/scribe/js/tryitout-5.7.0.js b/backend/public/vendor/scribe/js/tryitout-5.7.0.js
new file mode 100644
index 00000000..2a1d2b8a
--- /dev/null
+++ b/backend/public/vendor/scribe/js/tryitout-5.7.0.js
@@ -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;
+ });
+}
diff --git a/backend/resources/views/scribe/index.blade.php b/backend/resources/views/scribe/index.blade.php
new file mode 100644
index 00000000..5373812d
--- /dev/null
+++ b/backend/resources/views/scribe/index.blade.php
@@ -0,0 +1,668 @@
+
+
+
+
+
+
+ Headroom API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MENU
+
+
+
+
+
+
+ bash
+ javascript
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Introduction
+
Resource planning and capacity management API
+
+ Base URL : http://localhost/api
+
+
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.
+
+
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.
+
+
Authentication
+
+
Endpoints for JWT authentication and session lifecycle.
+
+
Login and get tokens
+
+
+requires authentication
+
+
+
Authenticate with email and password to receive an access token and refresh token.
+
+
+Example request:
+
+
+
+
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\"
+}"
+
+
+
+
+
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 => response.json());
+
+
+
+
+
+ Example 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"
+ }
+}
+
+
+ Example response (401):
+
+
+
+{
+ "message": "Invalid credentials"
+}
+
+
+ Example response (403):
+
+
+
+{
+ "message": "Account is inactive"
+}
+
+
+ Example response (422):
+
+
+
+{
+ "errors": {
+ "email": [
+ "The email field is required."
+ ],
+ "password": [
+ "The password field is required."
+ ]
+ }
+}
+
+
+
+ Received response :
+
+
+
+
+ Request failed with error:
+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
+
Refresh access token
+
+
+requires authentication
+
+
+
Exchange a valid refresh token for a new access token and refresh token pair.
+
+
+Example request:
+
+
+
+
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\"
+}"
+
+
+
+
+
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 => response.json());
+
+
+
+
+
+ Example response (200):
+
+
+
+{
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "refresh_token": "newtoken123",
+ "token_type": "bearer",
+ "expires_in": 3600
+}
+
+
+ Example response (401):
+
+
+
+{
+ "message": "Invalid or expired refresh token"
+}
+
+
+
+ Received response :
+
+
+
+
+ Request failed with error:
+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
+
Logout current session
+
+
+requires authentication
+
+
+
Invalidate a refresh token and end the active authenticated session.
+
+
+Example request:
+
+
+
+
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\"
+}"
+
+
+
+
+
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 => response.json());
+
+
+
+
+
+ Example response (200):
+
+
+
+{
+ "message": "Logged out successfully"
+}
+
+
+
+ Received response :
+
+
+
+
+ Request failed with error:
+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
+
+
+
+
+
+
+ bash
+ javascript
+
+
+
+
+
diff --git a/openspec/changes/p00-api-documentation/design.md b/openspec/changes/archive/2026-02-18-p00-api-documentation/design.md
similarity index 71%
rename from openspec/changes/p00-api-documentation/design.md
rename to openspec/changes/archive/2026-02-18-p00-api-documentation/design.md
index 8c12b252..4bb738c0 100644
--- a/openspec/changes/p00-api-documentation/design.md
+++ b/openspec/changes/archive/2026-02-18-p00-api-documentation/design.md
@@ -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
diff --git a/openspec/changes/p00-api-documentation/proposal.md b/openspec/changes/archive/2026-02-18-p00-api-documentation/proposal.md
similarity index 71%
rename from openspec/changes/p00-api-documentation/proposal.md
rename to openspec/changes/archive/2026-02-18-p00-api-documentation/proposal.md
index 913ddd06..bbf4709a 100644
--- a/openspec/changes/p00-api-documentation/proposal.md
+++ b/openspec/changes/archive/2026-02-18-p00-api-documentation/proposal.md
@@ -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
diff --git a/openspec/changes/archive/2026-02-18-p00-api-documentation/specs/api-documentation/spec.md b/openspec/changes/archive/2026-02-18-p00-api-documentation/specs/api-documentation/spec.md
new file mode 100644
index 00000000..18a0e7c8
--- /dev/null
+++ b/openspec/changes/archive/2026-02-18-p00-api-documentation/specs/api-documentation/spec.md
@@ -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
diff --git a/openspec/changes/archive/2026-02-18-p00-api-documentation/tasks.md b/openspec/changes/archive/2026-02-18-p00-api-documentation/tasks.md
new file mode 100644
index 00000000..6f895451
--- /dev/null
+++ b/openspec/changes/archive/2026-02-18-p00-api-documentation/tasks.md
@@ -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`
diff --git a/openspec/changes/p00-api-documentation/tasks.md b/openspec/changes/p00-api-documentation/tasks.md
deleted file mode 100644
index 822f6aa6..00000000
--- a/openspec/changes/p00-api-documentation/tasks.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# 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
-
-### 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`
diff --git a/openspec/specs/api-documentation/spec.md b/openspec/specs/api-documentation/spec.md
new file mode 100644
index 00000000..7ecdb6a3
--- /dev/null
+++ b/openspec/specs/api-documentation/spec.md
@@ -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