feat: Migrate OpenAPI documentation to static HTML structure

- Remove OpenApiDocs.php controller and swagger.php view
- Delete legacy bundler scripts (bundle-api-docs.php, bundle-api-docs.py)
- Remove old documentation files (API_DOCS_README.md, docs.html)
- Update Routes.php to remove OpenAPI documentation routes
- Modify PagesController.php to reflect changes
- Add new public/swagger/index.html as standalone documentation
This commit is contained in:
mahdahar 2026-02-16 15:58:30 +07:00
parent fcaf9b74ea
commit 46e52b124b
9 changed files with 236 additions and 794 deletions

View File

@ -21,12 +21,6 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
// Swagger API Documentation (public - no filters)
$routes->add('swagger', 'PagesController::swagger');
// OpenAPI Specification (server-side merged - public)
$routes->get('api-docs', 'OpenApiDocs::index');
// V2 Auth API Routes (public - no auth required)
$routes->group('v2/auth', function ($routes) {
$routes->post('login', 'AuthV2Controller::login');

View File

@ -1,43 +0,0 @@
<?php
namespace App\Controllers;
class OpenApiDocs extends BaseController
{
/**
* Serve pre-bundled OpenAPI specification
* Returns the bundled file with all paths and schemas resolved
*/
public function index()
{
try {
// Load pre-bundled OpenAPI spec
$bundledPath = FCPATH . 'api-docs.bundled.yaml';
if (!file_exists($bundledPath)) {
throw new \Exception("Bundled API docs not found: api-docs.bundled.yaml. Run: node bundle-api-docs.js");
}
$content = file_get_contents($bundledPath);
// Output as YAML
return $this->response
->setContentType('application/x-yaml')
->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
->setHeader('Access-Control-Allow-Headers', 'Content-Type')
->setHeader('Cache-Control', 'no-cache, must-revalidate')
->setBody($content);
} catch (\Exception $e) {
log_message('error', 'OpenApiDocs Error: ' . $e->getMessage());
return $this->response
->setStatusCode(500)
->setContentType('application/json')
->setBody(json_encode([
'error' => 'Failed to serve OpenAPI spec',
'message' => $e->getMessage()
]));
}
}
}

View File

@ -10,13 +10,5 @@ namespace App\Controllers;
*/
class PagesController extends BaseController
{
/**
* API Documentation / Swagger UI page
*/
public function swagger()
{
return view('swagger');
}
// Add page methods here as needed
}

View File

@ -1,405 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation - CLQMS</title>
<!-- Swagger UI CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
/* Custom Scrollbar for Webkit */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex; /* Sidebar layout */
}
/* Sidebar Styles */
#custom-sidebar {
width: 280px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
overflow-y: auto;
background: #fafafa;
border-right: 1px solid #ddd;
padding: 20px 0;
z-index: 1000;
transition: background 0.3s, border-color 0.3s;
}
#custom-sidebar h2 {
padding: 0 20px;
font-size: 1.2rem;
margin-bottom: 1rem;
color: #3b4151;
}
.sidebar-link {
display: block;
padding: 8px 20px;
color: #3b4151;
text-decoration: none;
font-size: 0.9rem;
border-left: 3px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.sidebar-link:hover {
background: rgba(0,0,0,0.05);
color: #000;
}
.sidebar-link.active {
border-left-color: #49cc90; /* Swagger Green */
background: rgba(73, 204, 144, 0.1);
font-weight: 600;
color: #49cc90;
}
/* Main Content Adjustments */
#swagger-ui {
margin-left: 280px; /* Width of sidebar */
width: calc(100% - 280px);
height: 100vh;
}
#swagger-ui .information-container {
margin: 0;
}
/* Dark theme support */
[data-theme="dark"] body {
background: #0d1117;
}
[data-theme="dark"] #custom-sidebar {
background: #161b22;
border-right: 1px solid #30363d;
}
[data-theme="dark"] #custom-sidebar h2 {
color: #c9d1d9;
}
[data-theme="dark"] .sidebar-link {
color: #8b949e;
}
[data-theme="dark"] .sidebar-link:hover {
background: rgba(255,255,255,0.05);
color: #c9d1d9;
}
[data-theme="dark"] .sidebar-link.active {
border-left-color: #58a6ff;
background: rgba(88, 166, 255, 0.1);
color: #58a6ff;
}
[data-theme="dark"] #swagger-ui {
--swagger-ui-color: #8b949e;
--swagger-ui-bg: #0d1117;
--swagger-ui-primary: #58a6ff;
--swagger-ui-text: #c9d1d9;
--swagger-ui-border: #30363d;
}
[data-theme="dark"] .swagger-ui {
color-scheme: dark;
}
[data-theme="dark"] .swagger-ui .info {
margin: 20px 0;
background: #161b22;
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
border: 1px solid #30363d;
}
[data-theme="dark"] .swagger-ui .info .title {
color: #58a6ff;
}
[data-theme="dark"] .swagger-ui .info .description {
color: #8b949e;
}
[data-theme="dark"] .swagger-ui .opblock {
background: #0d1117;
border: 1px solid #30363d;
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
}
[data-theme="dark"] .swagger-ui .opblock .opblock-summary {
border-color: #30363d;
background: #161b22;
}
[data-theme="dark"] .swagger-ui .opblock .opblock-summary-description {
color: #8b949e;
}
[data-theme="dark"] .swagger-ui .opblock.opblock-get {
border-color: #3fb950;
}
[data-theme="dark"] .swagger-ui .opblock.opblock-post {
border-color: #58a6ff;
}
[data-theme="dark"] .swagger-ui .opblock.opblock-put {
border-color: #d29922;
}
[data-theme="dark"] .swagger-ui .opblock.opblock-delete {
border-color: #f85149;
}
[data-theme="dark"] .swagger-ui .opblock.opblock-patch {
border-color: #a371f7;
}
[data-theme="dark"] .swagger-ui .scheme-container {
background: #161b22;
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
}
[data-theme="dark"] .swagger-ui .loading-container {
background: #0d1117;
}
[data-theme="dark"] .swagger-ui .btn {
background: #21262d;
border-color: #30363d;
color: #c9d1d9;
}
[data-theme="dark"] .swagger-ui .btn:hover {
background: #30363d;
}
[data-theme="dark"] .swagger-ui input[type="text"],
[data-theme="dark"] .swagger-ui textarea {
background: #0d1117;
border-color: #30363d;
color: #c9d1d9;
}
[data-theme="dark"] .swagger-ui select {
background: #0d1117;
border-color: #30363d;
color: #c9d1d9;
}
[data-theme="dark"] .swagger-ui .model-title {
color: #58a6ff;
}
[data-theme="dark"] .swagger-ui .property-name {
color: #8b949e;
}
[data-theme="dark"] .swagger-ui .tab li {
color: #8b949e;
}
[data-theme="dark"] .swagger-ui .tab li.active {
color: #58a6ff;
}
[data-theme="dark"] .swagger-ui .responses-inner h4,
[data-theme="dark"] .swagger-ui .responses-inner h5 {
color: #c9d1d9;
}
[data-theme="dark"] .swagger-ui .response-col_status {
color: #8b949e;
}
[data-theme="dark"] .swagger-ui .response-col_description {
color: #8b949e;
}
/* Responsive adjustments */
@media (max-width: 768px) {
body {
display: block; /* Stack on mobile */
}
#custom-sidebar {
width: 100%;
height: auto;
max-height: 200px;
position: relative;
border-right: none;
border-bottom: 1px solid #ddd;
}
#swagger-ui {
margin-left: 0;
width: 100%;
}
#swagger-ui .information-container {
padding: 10px;
}
#swagger-ui .info {
margin: 10px 0;
}
}
</style>
</head>
<body>
<div id="custom-sidebar">
<h2>Resources</h2>
<div id="sidebar-links"></div>
</div>
<div id="swagger-ui"></div>
<!-- Swagger UI Bundle -->
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
<script>
// Custom Sidebar Logic
function initCustomSidebar(ui) {
const sidebarLinks = document.getElementById('sidebar-links');
// Wait for Swagger UI to render tags
// We'll poll every 100ms for up to 5 seconds
let attempts = 0;
const maxAttempts = 50;
const interval = setInterval(() => {
attempts++;
const tags = document.querySelectorAll('.opblock-tag-section');
if (tags.length > 0 || attempts >= maxAttempts) {
clearInterval(interval);
buildSidebar(tags);
}
}, 100);
}
function buildSidebar(tags) {
const sidebarLinksContainer = document.getElementById('sidebar-links');
sidebarLinksContainer.innerHTML = ''; // Clear existing
if (tags.length === 0) {
sidebarLinksContainer.innerHTML = '<div style="padding:0 20px;color:#888;">No resources found</div>';
return;
}
tags.forEach(tagSection => {
const tagHeader = tagSection.querySelector('.opblock-tag');
if (!tagHeader) return;
const tagName = tagHeader.getAttribute('data-tag');
const tagText = tagHeader.querySelector('a span') ? tagHeader.querySelector('a span').innerText : tagName;
const tagId = tagHeader.id; // e.g., operations-tag-Auth
const link = document.createElement('a');
link.className = 'sidebar-link';
link.textContent = tagText;
link.href = '#' + tagId;
link.onclick = (e) => {
e.preventDefault();
const target = document.getElementById(tagId);
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
// Update active state
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
}
};
sidebarLinksContainer.appendChild(link);
});
}
window.onload = function() {
// Get JWT token from localStorage
// JWT token handled via HttpOnly cookie automatically
// Initialize Swagger UI
const ui = SwaggerUIBundle({
url: "<?= base_url('api-docs') ?>?v=<?= time() ?>",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
displayOperationId: false,
displayRequestDuration: true,
docExpansion: "list",
filter: true,
showRequestHeaders: true,
showCommonExtensions: true,
tryItOutEnabled: true,
persistAuthorization: true,
withCredentials: true, // Enable cookies
syntaxHighlight: {
activate: true,
theme: "monokai"
},
validatorUrl: null,
onComplete: () => {
// Initialize Custom Sidebar
initCustomSidebar(ui);
},
// Request interceptor to ensure cookies are sent
requestInterceptor: (request) => {
// Ensure credentials (cookies) are included in the request
request.credentials = 'include';
return request;
}
});
window.ui = ui;
};
</script>
</body>
</html>

View File

@ -1,162 +0,0 @@
# CLQMS OpenAPI Documentation Structure
## Overview
The OpenAPI documentation has been reorganized into a modular structure for better maintainability.
**Architecture:** Server-side resolution (CodeIgniter 4)
- Modular files in `paths/` and `components/schemas/` directories
- CodeIgniter controller merges files on-the-fly
- Single endpoint `/api-docs` serves complete specification
- No build step or bundling required
## How It Works
1. **Modular Development:** Edit files in `paths/` and `components/schemas/`
2. **Server-Side Merge:** `OpenApiDocs` controller loads and merges all files
3. **Single Endpoint:** Access `/api-docs` to get complete merged specification
4. **Cache Support:** Can add caching layer for production
## Recommended Usage
**For Swagger UI:** Use the CodeIgniter route `/api-docs` (already configured in `app/Views/swagger.php`)
**For development:** Edit modular files directly - changes are reflected immediately on next request
## Structure
```
public/
├── api-docs.yaml # Main entry point with schema references
├── api-docs.bundled.yaml # Bundled version (use this for Swagger UI)
├── components/
│ └── schemas/
│ ├── authentication.yaml
│ ├── common.yaml
│ ├── edge-api.yaml
│ ├── master-data.yaml
│ ├── orders.yaml
│ ├── organization.yaml
│ ├── patient-visit.yaml
│ ├── patient.yaml
│ ├── specimen.yaml
│ ├── tests.yaml
│ └── valuesets.yaml
├── paths/
│ ├── authentication.yaml
│ ├── demo.yaml
│ ├── edge-api.yaml
│ ├── master-data.yaml
│ ├── orders.yaml
│ ├── organization.yaml
│ ├── patient-visits.yaml
│ ├── patients.yaml
│ ├── results.yaml
│ ├── specimen.yaml
│ ├── tests.yaml
│ └── valuesets.yaml
└── bundle-api-docs.py # Bundler script
```
## Architecture
### Server-Side Resolution
The application uses a CodeIgniter 4 controller (`app/Controllers/OpenApiDocs.php`) that:
1. Loads `public/api-docs.yaml` (base spec with schemas)
2. Scans `public/paths/*.yaml` files
3. Merges all paths into the base spec
4. Outputs complete YAML response
### Benefits
- ✅ **No bundling step** - Files merged at runtime
- ✅ **Immediate updates** - Changes reflect immediately
- ✅ **Modular structure** - Separate files by domain
- ✅ **CI4 compatible** - Works with CodeIgniter 4
- ✅ **Nginx ready** - Can migrate to Nginx later
- ✅ **Cacheable** - Can add caching for production
## Usage
### For Development
Edit files directly:
- **Paths:** `public/paths/*.yaml`
- **Schemas:** `public/components/schemas/*.yaml`
### For Production/Swagger UI
**Endpoint:** `http://localhost/clqms01/api-docs`
The endpoint merges all files server-side and returns a complete OpenAPI specification that Swagger UI can consume.
## Caching (Optional)
For production, you can add caching to the `OpenApiDocs` controller:
```php
// In OpenApiDocs::index()
$cacheKey = 'openapi_spec_' . filemtime(FCPATH . 'api-docs.yaml');
if ($cached = cache()->get($cacheKey)) {
return $this->response->setBody($cached);
}
// ... build spec ...
// Cache for 5 minutes
cache()->save($cacheKey, $yaml, 300);
```
## Configuration Update
Update your application configuration to use the bundled file:
**PHP (CodeIgniter):**
```php
// Already updated in app/Views/swagger.php:
url: "<?= base_url('api-docs.bundled.yaml') ?>"
```
**JavaScript/Node:**
```javascript
// Change from:
const spec = await fetch('/api-docs.yaml');
// To:
const spec = await fetch('/api-docs.bundled.yaml');
```
## Benefits
1. **Modular**: Each domain has its own file
2. **Maintainable**: Easier to find and update specific endpoints
3. **Team-friendly**: Multiple developers can work on different files
4. **Schema Reuse**: Schemas defined once, referenced everywhere
5. **Better Versioning**: Individual domains can be versioned separately
## Migration Notes
- **From bundled to server-side:** Already done! The system now uses server-side resolution.
- **Nginx migration:** When ready to migrate to Nginx, the approach remains the same (CI4 handles the merging).
- **Back to bundling:** If needed, the bundler script is still available: `python bundle-api-docs.py`
## Troubleshooting
### Schema Resolution Errors
If you see errors like "Could not resolve reference", ensure:
1. Schema files exist in `components/schemas/`
2. References use correct relative paths (e.g., `./components/schemas/patient.yaml#/Patient`)
3. You're using the bundled file (`api-docs.bundled.yaml`) for OpenAPI tools
### Path Resolution Errors
OpenAPI 3.0 doesn't support external file references for paths. Always use the bundled version for serving the documentation.
## Migration Notes
- The original `api-docs.yaml` is preserved with schema references only
- All paths have been moved to individual files in `paths/` directory
- A bundler script merges everything into `api-docs.bundled.yaml`
- Update your application to serve `api-docs.bundled.yaml` instead

View File

@ -1,55 +0,0 @@
<?php
/**
* OpenAPI Documentation Bundler
*
* This script merges the modular OpenAPI specification files into a single
* api-docs.yaml file that can be served to Swagger UI or other OpenAPI tools.
*
* Usage: php bundle-api-docs.php
*/
$publicDir = __DIR__;
$componentsDir = $publicDir . '/components/schemas';
$pathsDir = $publicDir . '/paths';
$outputFile = $publicDir . '/api-docs.bundled.yaml';
// Read the base api-docs.yaml
$apiDocsContent = file_get_contents($publicDir . '/api-docs.yaml');
$apiDocs = yaml_parse($apiDocsContent);
if ($apiDocs === false) {
die("Error: Failed to parse api-docs.yaml\n");
}
// Merge paths from all path files
$paths = [];
$pathFiles = glob($pathsDir . '/*.yaml');
echo "Found " . count($pathFiles) . " path files to merge...\n";
foreach ($pathFiles as $filepath) {
$filename = basename($filepath);
$content = file_get_contents($filepath);
$parsed = yaml_parse($content);
if ($parsed === false) {
echo " ⚠ Warning: Failed to parse $filename\n";
continue;
}
$paths = array_merge($paths, $parsed);
echo " ✓ Merged " . count($parsed) . " paths from $filename\n";
}
// Replace the empty paths with merged paths
$apiDocs['paths'] = $paths;
// Write the bundled file
$bundledYaml = yaml_emit($apiDocs, YAML_UTF8_ENCODING);
file_put_contents($outputFile, $bundledYaml);
echo "\n✅ Successfully bundled OpenAPI spec to: $outputFile\n";
echo " Total paths: " . count($paths) . "\n";
echo " Total schemas: " . count($apiDocs['components']['schemas']) . "\n";

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""
OpenAPI Documentation Bundler
This script merges the modular OpenAPI specification files into a single
api-docs.bundled.yaml file that can be served to Swagger UI or other OpenAPI tools.
It merges paths from the paths/ directory and then uses Redocly CLI to resolve
all $ref references into inline definitions.
Usage: python bundle-api-docs.py
"""
import yaml
import os
import glob
import subprocess
public_dir = os.path.dirname(os.path.abspath(__file__))
components_dir = os.path.join(public_dir, "components", "schemas")
paths_dir = os.path.join(public_dir, "paths")
temp_file = os.path.join(public_dir, "api-docs.merged.yaml")
output_file = os.path.join(public_dir, "api-docs.bundled.yaml")
# Read the base api-docs.yaml
with open(os.path.join(public_dir, "api-docs.yaml"), "r", encoding="utf-8") as f:
api_docs = yaml.safe_load(f)
# Merge paths from all path files
paths = {}
path_files = glob.glob(os.path.join(paths_dir, "*.yaml"))
print(f"Found {len(path_files)} path files to merge...")
for filepath in path_files:
filename = os.path.basename(filepath)
try:
with open(filepath, "r", encoding="utf-8") as f:
parsed = yaml.safe_load(f)
if parsed:
paths.update(parsed)
print(f" [OK] Merged {len(parsed)} paths from {filename}")
except Exception as e:
print(f" [WARN] Failed to parse {filename}: {e}")
# Replace the empty paths with merged paths
api_docs["paths"] = paths
# Write the merged file (still has $refs)
with open(temp_file, "w", encoding="utf-8") as f:
yaml.dump(
api_docs,
f,
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
width=4096,
)
print(f"\n[SUCCESS] Merged paths into: {temp_file}")
print(f" Total paths: {len(paths)}")
# Now use Redocly CLI to resolve all $ref references
print("\nResolving $ref references with Redocly CLI...")
try:
result = subprocess.run(
["redocly", "bundle", temp_file, "-o", output_file],
capture_output=True,
text=True,
cwd=public_dir,
)
if result.returncode == 0:
print(f" [SUCCESS] Resolved all $ref references to: {output_file}")
# Clean up temp file
os.remove(temp_file)
print(f" [CLEANUP] Removed temp file: {temp_file}")
else:
print(f" [ERROR] Redocly CLI failed:")
print(f" {result.stderr}")
exit(1)
except FileNotFoundError:
print(" [ERROR] Redocly CLI not found. Install with: npm install -g @redocly/cli")
exit(1)
except Exception as e:
print(f" [ERROR] Failed to run Redocly CLI: {e}")
exit(1)
print(f"\n{'=' * 60}")
print(f"Bundling complete!")
print(f" Output: {output_file}")
print(f" Total paths: {len(paths)}")
print(f" Total schemas: {len(api_docs.get('components', {}).get('schemas', {}))}")
print(f"\nYou can now serve this file to Swagger UI.")
print(f"{'=' * 60}")

View File

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Elements in HTML</title>
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
</head>
<body>
<elements-api apiDescriptionUrl="openapi.yaml" router="hash" layout="sidebar" />
</body>
</html>

235
public/swagger/index.html Normal file
View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLQMS API Documentation</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
<style>
* { box-sizing: border-box; }
:root {
--sidebar-bg: #f8f9fa;
--sidebar-border: #dee2e6;
--text-primary: #212529;
--text-secondary: #495057;
--accent-color: #49cc90;
--hover-bg: rgba(0,0,0,0.05);
--active-bg: rgba(73, 204, 144, 0.1);
--toggle-bg: #e9ecef;
}
[data-theme="dark"] {
--sidebar-bg: #1a1a1a;
--sidebar-border: #333;
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--accent-color: #5cb85c;
--hover-bg: rgba(255,255,255,0.05);
--active-bg: rgba(92, 184, 92, 0.15);
--toggle-bg: #333;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
}
#sidebar {
width: 260px;
height: 100vh;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
position: fixed;
left: 0;
top: 0;
padding: 20px 0;
transition: background 0.3s, border-color 0.3s;
}
#sidebar h2 {
margin: 0 0 20px 0;
padding: 0 20px;
font-size: 18px;
color: var(--text-primary);
font-weight: 600;
}
.nav-link {
display: block;
padding: 10px 20px;
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
border-left: 3px solid transparent;
transition: all 0.2s;
cursor: pointer;
}
.nav-link:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.nav-link.active {
border-left-color: var(--accent-color);
background: var(--active-bg);
color: var(--text-primary);
font-weight: 500;
}
#swagger-ui {
margin-left: 260px;
width: calc(100% - 260px);
min-height: 100vh;
}
[data-theme="dark"] body {
background: #0d1117;
}
[data-theme="dark"] #swagger-ui {
background: #0d1117;
}
[data-theme="dark"] .swagger-ui {
filter: invert(100%) hue-rotate(180deg);
}
[data-theme="dark"] .swagger-ui .topbar img,
[data-theme="dark"] .swagger-ui svg,
[data-theme="dark"] .swagger-ui img,
[data-theme="dark"] .swagger-ui .authorization__btn svg,
[data-theme="dark"] .swagger-ui .expand-methods svg,
[data-theme="dark"] .swagger-ui .expand-operation svg,
[data-theme="dark"] .swagger-ui .copy-to-clipboard svg,
[data-theme="dark"] .swagger-ui .opblock-control-arrow svg {
filter: invert(100%) hue-rotate(180deg);
}
.theme-toggle {
position: fixed;
bottom: 20px;
left: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--toggle-bg);
border: 2px solid var(--sidebar-border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.theme-toggle:hover {
transform: scale(1.1);
}
@media (max-width: 768px) {
body { display: block; }
#sidebar {
width: 100%;
height: auto;
max-height: 200px;
position: relative;
border-right: none;
border-bottom: 1px solid var(--sidebar-border);
}
#swagger-ui {
margin-left: 0;
width: 100%;
}
.theme-toggle {
bottom: 10px;
right: 10px;
left: auto;
}
}
</style>
</head>
<body>
<nav id="sidebar">
<h2>Resources</h2>
<div id="nav-links"></div>
</nav>
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
<span id="themeIcon">🌙</span>
</button>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
// Theme management
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const html = document.documentElement;
const savedTheme = localStorage.getItem('swagger-theme') || 'light';
html.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('swagger-theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
themeIcon.textContent = theme === 'light' ? '🌙' : '☀️';
}
const ui = SwaggerUIBundle({
url: '../api-docs.bundled.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis],
layout: 'BaseLayout',
onComplete: () => buildSidebar()
});
function buildSidebar() {
const container = document.getElementById('nav-links');
const tags = document.querySelectorAll('.opblock-tag-section');
if (!tags.length) {
setTimeout(buildSidebar, 500);
return;
}
container.innerHTML = '';
tags.forEach(tag => {
const header = tag.querySelector('.opblock-tag');
if (!header) return;
const name = header.getAttribute('data-tag') || header.textContent;
const id = header.id;
const link = document.createElement('a');
link.className = 'nav-link';
link.textContent = name;
link.onclick = (e) => {
e.preventDefault();
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
};
container.appendChild(link);
});
}
</script>
</body>
</html>