Merge pull request #60 from builderz-labs/feat/openapi-docs

feat: OpenAPI 3.1 documentation with Scalar UI
This commit is contained in:
nyk 2026-03-02 11:04:11 +07:00 committed by GitHub
commit e88942e8f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 4352 additions and 9 deletions

1695
openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "mission-control",
"version": "1.2.0",
"description": "OpenClaw Mission Control \u2014 open-source agent orchestration dashboard",
"description": "OpenClaw Mission Control open-source agent orchestration dashboard",
"scripts": {
"dev": "next dev --hostname 127.0.0.1",
"build": "next build",
@ -16,6 +16,7 @@
"quality:gate": "pnpm test:all"
},
"dependencies": {
"@scalar/api-reference-react": "^0.8.66",
"@xyflow/react": "^12.10.0",
"autoprefixer": "^10.4.20",
"better-sqlite3": "^12.6.2",

File diff suppressed because it is too large Load Diff

19
src/app/api/docs/route.ts Normal file
View File

@ -0,0 +1,19 @@
import { NextResponse } from 'next/server'
import { readFileSync } from 'fs'
import { join } from 'path'
let cachedSpec: string | null = null
export async function GET() {
if (!cachedSpec) {
const specPath = join(process.cwd(), 'openapi.json')
cachedSpec = readFileSync(specPath, 'utf-8')
}
return new NextResponse(cachedSpec, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
},
})
}

27
src/app/docs/page.tsx Normal file
View File

@ -0,0 +1,27 @@
'use client'
import { ApiReferenceReact } from '@scalar/api-reference-react'
import '@scalar/api-reference-react/style.css'
export default function DocsPage() {
return (
<div className="h-screen">
<ApiReferenceReact
configuration={{
url: '/api/docs',
theme: 'kepler',
darkMode: true,
hideModels: false,
hideDownloadButton: false,
defaultHttpClient: {
targetKey: 'shell',
clientKey: 'curl',
},
metaData: {
title: 'Mission Control API Docs',
},
}}
/>
</div>
)
}

View File

@ -91,8 +91,8 @@ export function middleware(request: NextRequest) {
}
}
// Allow login page and auth API without session
if (pathname === '/login' || pathname.startsWith('/api/auth/')) {
// Allow login page, auth API, and docs without session
if (pathname === '/login' || pathname.startsWith('/api/auth/') || pathname === '/api/docs' || pathname === '/docs') {
return applySecurityHeaders(NextResponse.next())
}

36
tests/openapi.spec.ts Normal file
View File

@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test'
test.describe('OpenAPI Documentation', () => {
test('GET /api/docs returns valid OpenAPI 3.1 JSON', async ({ request }) => {
const res = await request.get('/api/docs')
expect(res.status()).toBe(200)
expect(res.headers()['content-type']).toContain('application/json')
const spec = await res.json()
expect(spec.openapi).toMatch(/^3\.1/)
expect(spec.info).toBeDefined()
expect(spec.info.title).toBe('Mission Control API')
expect(spec.info.version).toBeDefined()
expect(spec.paths).toBeDefined()
})
test('GET /api/docs includes key paths', async ({ request }) => {
const res = await request.get('/api/docs')
const spec = await res.json()
// Verify core paths exist
const paths = Object.keys(spec.paths)
expect(paths).toContain('/api/agents')
expect(paths).toContain('/api/tasks')
expect(paths).toContain('/api/tokens')
expect(paths).toContain('/api/auth/login')
})
test('GET /api/docs is accessible without auth', async ({ request }) => {
// No API key header, no session cookie
const res = await request.get('/api/docs', { headers: {} })
expect(res.status()).toBe(200)
const spec = await res.json()
expect(spec.openapi).toBeDefined()
})
})