feat(api-docs): enhance in-panel API documentation (#4312)

* feat(api-docs): enhance API documentation with missing endpoints, search, collapse, and route sync test

- Add 29 undocumented routes across 4 new sections (Settings, Xray Settings,
  Subscription Server, WebSocket) plus 4 missing Server API endpoints
- Fix inaccuracies: history metric keys, node metric keys, VLESS enc description
- Add response schemas to 15+ key endpoints
- Add search bar and expand/collapse all controls to the docs page
- Add collapsible endpoint sections with endpoint count
- Add Go test (TestAPIRoutesDocumented) to verify all Go routes are documented

* feat(api-docs): add JSON syntax highlighting and top-right copy button to code blocks

* fix(api-docs): use distinct colors for JSON syntax highlighting (green strings, amber numbers)

* feat(api-docs): add request body examples, error responses, WebSocket message types, and subscription response headers

* fix(api-docs): use ClipboardManager.copyText instead of copy to fix API token copy button
This commit is contained in:
Abdalrahman
2026-05-13 02:47:09 +03:00
committed by GitHub
parent 9f7e8178d4
commit 6e12329d9d
6 changed files with 859 additions and 28 deletions
+104 -5
View File
@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { Modal, message } from 'ant-design-vue';
import {
@@ -8,13 +8,17 @@ import {
CopyOutlined,
EyeOutlined,
EyeInvisibleOutlined,
SearchOutlined,
ExpandOutlined,
CompressOutlined,
} from '@ant-design/icons-vue';
import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
import AppSidebar from '@/components/AppSidebar.vue';
import { HttpUtil, ClipboardManager } from '@/utils/index.js';
import { sections } from './endpoints.js';
import { sections as allSections } from './endpoints.js';
import EndpointSection from './EndpointSection.vue';
import CodeBlock from './CodeBlock.vue';
const { t } = useI18n();
@@ -26,11 +30,55 @@ const tokenLoading = ref(false);
const tokenRotating = ref(false);
const tokenVisible = ref(false);
const searchQuery = ref('');
const collapsedSections = ref(new Set());
const curlExample = `curl -X GET \\
-H "Authorization: Bearer YOUR_API_TOKEN" \\
-H "Accept: application/json" \\
https://your-panel.example.com/panel/api/inbounds/list`;
const sections = computed(() => {
const q = searchQuery.value.toLowerCase().trim();
if (!q) return allSections;
return allSections
.map(s => {
const matching = s.endpoints.filter(e =>
e.path.toLowerCase().includes(q) ||
e.summary?.toLowerCase().includes(q) ||
e.method.toLowerCase().includes(q)
);
return { ...s, endpoints: matching };
})
.filter(s => s.endpoints.length > 0);
});
const endpointCount = computed(() =>
allSections.reduce((sum, s) => sum + s.endpoints.length, 0)
);
const visibleSections = computed(() =>
sections.value.reduce((sum, s) => sum + s.endpoints.length, 0)
);
function isCollapsed(id) {
return collapsedSections.value.has(id);
}
function toggleSection(id) {
const s = new Set(collapsedSections.value);
if (s.has(id)) s.delete(id); else s.add(id);
collapsedSections.value = s;
}
function expandAll() {
collapsedSections.value = new Set();
}
function collapseAll() {
collapsedSections.value = new Set(allSections.map(s => s.id));
}
async function loadApiToken() {
tokenLoading.value = true;
try {
@@ -93,6 +141,7 @@ onMounted(() => {
cookie, or with the <code>Authorization: Bearer &lt;token&gt;</code> header below. Every endpoint
returns a uniform <code>{ success, msg, obj }</code> envelope unless otherwise noted.
</p>
</header>
<a-card class="token-card" size="small">
@@ -135,18 +184,48 @@ onMounted(() => {
</a-card>
<a-card class="curl-card" size="small" title="Quick example">
<pre class="code-block">{{ curlExample }}</pre>
<CodeBlock :code="curlExample" lang="text" />
</a-card>
<div class="toolbar">
<a-input-search
v-model:value="searchQuery"
placeholder="Search endpoints by path, method, or description…"
allow-clear
class="search-bar"
>
<template #prefix><SearchOutlined /></template>
</a-input-search>
<span class="match-count" v-if="searchQuery">
{{ visibleSections }} / {{ endpointCount }} endpoints
</span>
<a-space size="small">
<a-button size="small" @click="expandAll">
<template #icon><ExpandOutlined /></template>
Expand all
</a-button>
<a-button size="small" @click="collapseAll">
<template #icon><CompressOutlined /></template>
Collapse all
</a-button>
</a-space>
</div>
<nav class="toc-nav">
<span class="toc-label">On this page:</span>
<a v-for="s in sections" :key="s.id" class="toc-link" :href="`#${s.id}`"
@click.prevent="scrollToSection(s.id)">
{{ s.title }}
{{ s.title }} ({{ s.endpoints.length }})
</a>
</nav>
<EndpointSection v-for="s in sections" :key="s.id" :section="s" />
<EndpointSection
v-for="s in sections"
:key="s.id"
:section="s"
:collapsed="isCollapsed(s.id)"
@toggle="toggleSection(s.id)"
/>
</div>
</a-layout-content>
</a-layout>
@@ -275,6 +354,26 @@ onMounted(() => {
overflow-x: auto;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.search-bar {
flex: 1;
min-width: 200px;
max-width: 480px;
}
.match-count {
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
white-space: nowrap;
}
.toc-nav {
display: flex;
flex-wrap: wrap;