| (function () { |
| const state = { |
| section: null, |
| loaded: false, |
| loading: false, |
| spec: null, |
| routes: [], |
| schemas: [], |
| services: [], |
| providerCount: 0, |
| capabilityCount: 0, |
| groups: [], |
| search: '', |
| sectionReady: false, |
| loadPromise: null, |
| }; |
|
|
| const FallbackRoutes = [ |
| { method: 'GET', path: '/health', summary: 'Core health check', group: 'Core App & Health' }, |
| { method: 'GET', path: '/api/health', summary: 'API health check', group: 'Core App & Health' }, |
| { method: 'GET', path: '/api/status', summary: 'System status summary', group: 'Core App & Health' }, |
| { method: 'GET', path: '/api/market', summary: 'Current market overview', group: 'Market Data & Signals' }, |
| { method: 'GET', path: '/api/sentiment', summary: 'Fear & Greed sentiment', group: 'Market Data & Signals' }, |
| { method: 'GET', path: '/api/resources/summary', summary: 'Resource summary', group: 'Resources & Registry' }, |
| { method: 'GET', path: '/api/providers/status', summary: 'Provider status summary', group: 'Resources & Registry' }, |
| { method: 'GET', path: '/api/short-hunter/health', summary: 'Short Hunter gateway health', group: 'Short Hunter Gateway' }, |
| { method: 'GET', path: '/api/short-hunter/capabilities', summary: 'Gateway capabilities', group: 'Short Hunter Gateway' }, |
| { method: 'GET', path: '/api/short-hunter/providers/status', summary: 'Gateway provider status', group: 'Short Hunter Gateway' }, |
| { method: 'GET', path: '/api/short-hunter/snapshot/{symbol}', summary: 'Normalized futures snapshot', group: 'Short Hunter Gateway' }, |
| { method: 'GET', path: '/api/coins/top', summary: 'Top coins compatibility endpoint', group: 'Legacy Compatibility' }, |
| { method: 'GET', path: '/api/ohlcv', summary: 'OHLCV compatibility endpoint', group: 'Legacy Compatibility' }, |
| { method: 'POST', path: '/api/sentiment/analyze', summary: 'Sentiment analysis', group: 'AI & Models' }, |
| { method: 'GET', path: '/api/news/latest', summary: 'Latest news articles', group: 'News & External Data' }, |
| ]; |
|
|
| const FallbackSchemas = [ |
| { name: 'HealthResponse', description: 'Service health payload', sample: { status: 'healthy', timestamp: '2026-06-15T00:00:00Z' } }, |
| { name: 'ShortHunterSnapshot', description: 'Normalized market snapshot', sample: { success: true, sourceMode: 'LIVE', dataState: 'REAL', data: { ticker: {}, ohlcv: [] } } }, |
| ]; |
|
|
| const ServiceManifest = [ |
| { |
| name: 'FastAPI app', |
| module: 'api_server_extended.py', |
| role: 'Primary HTTP application, HTML shell, status routes, resource registry, diagnostics, models, news, and legacy compatibility routes.', |
| }, |
| { |
| name: 'Short Hunter gateway', |
| module: 'short_hunter_routes.py', |
| role: 'Capability-based datasource gateway with rotation, fallback, cache, health, and no-trade guard responses.', |
| }, |
| { |
| name: 'Legacy compatibility layer', |
| module: 'api_compat_routes.py', |
| role: 'Preserves older market, sentiment, news, provider, and indicator endpoints for existing consumers.', |
| }, |
| { |
| name: 'Provider registry', |
| module: 'providers/registry.py', |
| role: 'Safely loads catalog JSON and exposes provider capabilities, priorities, and auth requirements.', |
| }, |
| { |
| name: 'Smart router', |
| module: 'providers/router.py', |
| role: 'Chooses providers per capability and reports degraded or unavailable states honestly.', |
| }, |
| { |
| name: 'Frontend API client', |
| module: 'static/js/apiClient.js', |
| role: 'Shared browser fetch wrapper for tabs and page widgets.', |
| }, |
| ]; |
|
|
| const SectionIds = [ |
| { id: 'overview', label: 'Overview' }, |
| { id: 'apis', label: 'APIs' }, |
| { id: 'data-models', label: 'Data Models' }, |
| { id: 'integration', label: 'Integration Guide' }, |
| { id: 'examples', label: 'Examples' }, |
| ]; |
|
|
| const methodTone = { |
| GET: 'success', |
| POST: 'info', |
| PUT: 'warning', |
| PATCH: 'warning', |
| DELETE: 'danger', |
| HEAD: 'info', |
| OPTIONS: 'info', |
| }; |
|
|
| const icon = { |
| refresh: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>', |
| copy: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>', |
| search: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>', |
| link: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.07 0l2.12-2.12a5 5 0 1 0-7.07-7.07L11 5"></path><path d="M14 11a5 5 0 0 0-7.07 0L4.81 13.12a5 5 0 1 0 7.07 7.07L13 19"></path></svg>', |
| }; |
|
|
| function ensureStyles() { |
| if (document.getElementById('help-reference-styles')) return; |
| const style = document.createElement('style'); |
| style.id = 'help-reference-styles'; |
| style.textContent = ` |
| .help-page-shell{display:grid;gap:16px} |
| .help-hero-card,.help-section,.help-toolbar,.help-service-card,.help-model-card,.help-example-card,.help-route-card,.help-route-group,.help-note-card{backdrop-filter:blur(18px)} |
| .help-hero-top,.help-toolbar,.help-section-head,.help-service-title,.help-model-head,.help-example-head,.help-route-summary,.help-route-summary-meta,.help-code-head,.help-copy-row{display:flex;gap:12px;align-items:flex-start} |
| .help-hero-top,.help-section-head,.help-example-head,.help-model-head,.help-service-title{justify-content:space-between} |
| .help-hero-card{padding:20px;background:linear-gradient(135deg,rgba(102,126,234,.18),rgba(16,185,129,.08));border:1px solid rgba(255,255,255,.08);border-radius:16px} |
| .help-kicker{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-secondary);margin-bottom:8px} |
| .help-hero-card h3{margin-bottom:10px;font-size:22px} |
| .help-hero-card p{max-width:72ch} |
| .help-hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end} |
| .help-action-btn,.help-copy-btn,.help-nav-link{border:none;cursor:pointer} |
| .help-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px} |
| .help-stat-card{padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.03);display:grid;gap:4px} |
| .help-stat-label{color:var(--text-secondary);font-size:12px;text-transform:uppercase;letter-spacing:.06em} |
| .help-toolbar{justify-content:space-between;align-items:center;flex-wrap:wrap;padding:14px 16px} |
| .help-search{display:flex;align-items:center;gap:10px;flex:1;min-width:260px;padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.05)} |
| .help-search input{width:100%;border:none;outline:none;background:transparent;color:var(--text-primary);font:inherit} |
| .help-toolbar-meta{display:flex;gap:8px;flex-wrap:wrap} |
| .help-layout{display:grid;grid-template-columns:220px minmax(0,1fr);gap:16px;align-items:start} |
| .help-nav{position:sticky;top:16px;display:grid;gap:8px;padding:16px} |
| .help-nav-link{width:100%;text-align:left;padding:11px 12px;border:1px solid transparent;border-radius:10px;background:rgba(255,255,255,.03);color:var(--text-secondary);transition:all .2s ease} |
| .help-nav-link:hover{color:var(--text-primary);border-color:var(--border);background:rgba(255,255,255,.08)} |
| .help-content{display:grid;gap:16px} |
| .help-section{padding:18px;border:1px solid rgba(255,255,255,.08);border-radius:16px;background:rgba(8,12,24,.68)} |
| .help-grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px} |
| .help-card-grid,.help-model-grid,.help-example-grid{display:grid;gap:14px} |
| .help-service-card,.help-model-card,.help-example-card{padding:16px;border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.03)} |
| .help-route-group{border:1px solid var(--border);border-radius:14px;overflow:hidden;background:rgba(255,255,255,.02)} |
| .help-route-group>summary{list-style:none;cursor:pointer;padding:14px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;background:rgba(255,255,255,.04)} |
| .help-route-group>summary::-webkit-details-marker,.help-route-card>summary::-webkit-details-marker{display:none} |
| .help-route-list{display:grid;gap:12px;padding:12px} |
| .help-route-card{border:1px solid var(--border);border-radius:12px;background:rgba(0,0,0,.18);overflow:hidden} |
| .help-route-card>summary{cursor:pointer;list-style:none;padding:14px 16px;display:grid;gap:8px} |
| .help-route-path{font-family:'JetBrains Mono',monospace;color:var(--text-primary);word-break:break-word} |
| .help-route-body{padding:0 16px 16px;display:grid;gap:14px} |
| .help-copy-btn{display:inline-flex;align-items:center;gap:6px;padding:9px 11px;border:1px solid var(--border);border-radius:10px;background:rgba(255,255,255,.06);color:var(--text-primary);transition:all .2s ease} |
| .help-copy-btn:hover{background:rgba(255,255,255,.12);transform:translateY(-1px)} |
| .help-code-block{border:1px solid var(--border);border-radius:12px;overflow:hidden;background:rgba(0,0,0,.24)} |
| .help-code-head{justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid var(--border)} |
| .help-code{margin:0;padding:12px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.6;color:var(--text-primary)} |
| .help-params{display:grid;gap:12px} |
| .help-param-row{display:grid;gap:8px;padding:12px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,.03)} |
| .help-param-meta{color:var(--text-secondary);font-size:12px;text-transform:uppercase;letter-spacing:.06em} |
| .help-inline-code{display:inline-block;padding:4px 8px;border-radius:8px;background:rgba(255,255,255,.06);color:var(--text-primary);font-family:'JetBrains Mono',monospace;font-size:12px;overflow-x:auto} |
| .help-empty-state{padding:18px;border:1px dashed var(--border);border-radius:12px;text-align:center;color:var(--text-secondary);background:rgba(255,255,255,.02)} |
| .help-note-card{padding:16px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.03)} |
| .is-hidden{display:none!important} |
| @media (max-width:1080px){.help-layout{grid-template-columns:1fr}.help-nav{position:relative;top:auto;grid-template-columns:repeat(2,minmax(0,1fr))}} |
| @media (max-width:768px){.help-hero-top,.help-toolbar,.help-section-head,.help-service-title,.help-model-head,.help-example-head,.help-grid-2{flex-direction:column}.help-nav{grid-template-columns:1fr}} |
| `; |
| document.head.appendChild(style); |
| } |
|
|
| function toast(type, message) { |
| const manager = window.toastManager || window.toast; |
| if (manager && typeof manager[type] === 'function') { |
| manager[type](message); |
| } else { |
| console.log(`[${type}] ${message}`); |
| } |
| } |
|
|
| function escapeHtml(value) { |
| return String(value ?? '') |
| .replace(/&/g, '&') |
| .replace(/</g, '<') |
| .replace(/>/g, '>') |
| .replace(/"/g, '"') |
| .replace(/'/g, '''); |
| } |
|
|
| function toTitleCase(value) { |
| return String(value || '') |
| .replace(/[_-]+/g, ' ') |
| .replace(/\s+/g, ' ') |
| .trim() |
| .replace(/\b\w/g, (m) => m.toUpperCase()); |
| } |
|
|
| function chunk(value, max = 120) { |
| if (!value) return ''; |
| return value.length > max ? `${value.slice(0, max)}...` : value; |
| } |
|
|
| function resolveRef(ref, components) { |
| if (!ref || typeof ref !== 'string' || !ref.startsWith('#/')) return null; |
| const parts = ref.replace(/^#\//, '').split('/'); |
| let current = { components }; |
| for (const part of parts) { |
| current = current?.[part]; |
| if (!current) return null; |
| } |
| return current; |
| } |
|
|
| function unwrapSchema(schema, components, depth = 0) { |
| if (!schema || depth > 6) return null; |
| if (schema.$ref) return unwrapSchema(resolveRef(schema.$ref, components), components, depth + 1); |
| if (schema.allOf && schema.allOf.length) return unwrapSchema(schema.allOf[0], components, depth + 1); |
| if (schema.oneOf && schema.oneOf.length) return unwrapSchema(schema.oneOf[0], components, depth + 1); |
| if (schema.anyOf && schema.anyOf.length) return unwrapSchema(schema.anyOf[0], components, depth + 1); |
| return schema; |
| } |
|
|
| function sampleScalar(schema, key = '') { |
| const name = key.toLowerCase(); |
| if (schema?.enum?.length) return schema.enum[0]; |
| if (name.includes('symbol')) return 'BTCUSDT'; |
| if (name.includes('provider')) return 'kucoin_futures'; |
| if (name.includes('category')) return 'market'; |
| if (name.includes('interval') || name.includes('timeframe')) return '1h'; |
| if (name.includes('limit')) return 100; |
| if (name.includes('page')) return 1; |
| if (name.includes('query')) return 'bitcoin'; |
| if (name.includes('text')) return 'Bitcoin breaks resistance'; |
| if (name.includes('title')) return 'Example headline'; |
| if (name.includes('body') || name.includes('content') || name.includes('message')) return 'Example content'; |
| if (name.includes('coin')) return 'BTC'; |
| if (name.includes('coin_id')) return 'bitcoin'; |
| if (name.includes('export_type')) return 'json'; |
| if (name.includes('name')) return 'Example name'; |
| if (name.includes('id')) return 'example-id'; |
| if (name.includes('mode')) return 'auto'; |
| if (name.includes('source')) return 'all'; |
| if (name.includes('window')) return '24h'; |
| if (name.includes('sort')) return 'publishedAt'; |
| if (name.includes('time')) return '2026-06-15T00:00:00Z'; |
| if (schema?.format === 'date-time') return '2026-06-15T00:00:00Z'; |
| if (schema?.format === 'date') return '2026-06-15'; |
| if (schema?.format === 'uuid') return '00000000-0000-0000-0000-000000000000'; |
| if (schema?.type === 'integer' || schema?.type === 'number') return 0; |
| if (schema?.type === 'boolean') return true; |
| return 'string'; |
| } |
|
|
| function sampleFromSchema(schema, components, key = '', depth = 0) { |
| const resolved = unwrapSchema(schema, components, depth); |
| if (!resolved || depth > 6) return null; |
| if (resolved.example !== undefined) return resolved.example; |
| if (resolved.examples && !Array.isArray(resolved.examples)) { |
| const first = Object.values(resolved.examples)[0]; |
| if (first && first.value !== undefined) return first.value; |
| } |
| if (resolved.enum?.length) return resolved.enum[0]; |
|
|
| const type = resolved.type || (resolved.properties ? 'object' : null); |
| if (type === 'object' || resolved.properties) { |
| const output = {}; |
| const props = resolved.properties || {}; |
| const required = new Set(resolved.required || []); |
| const keys = [ |
| ...Object.keys(props).filter((item) => required.has(item)), |
| ...Object.keys(props).filter((item) => !required.has(item)), |
| ]; |
| keys.slice(0, 10).forEach((propName) => { |
| output[propName] = sampleFromSchema(props[propName], components, propName, depth + 1); |
| }); |
| return output; |
| } |
|
|
| if (type === 'array') { |
| const item = sampleFromSchema(resolved.items, components, key, depth + 1); |
| return item === null ? [] : [item]; |
| } |
|
|
| return sampleScalar(resolved, key); |
| } |
|
|
| function schemaTypeLabel(schema, components) { |
| const resolved = unwrapSchema(schema, components); |
| if (!resolved) return 'unknown'; |
| if (resolved.enum?.length) return `enum (${resolved.enum.length})`; |
| if (resolved.type) { |
| if (resolved.type === 'array') { |
| return `array<${schemaTypeLabel(resolved.items, components)}>`; |
| } |
| return resolved.type; |
| } |
| if (resolved.properties) return 'object'; |
| return 'unknown'; |
| } |
|
|
| function operationGroup(path, tags = []) { |
| const tag = tags[0] || ''; |
| if (tag.includes('Short Hunter')) return 'Short Hunter Gateway'; |
| if (tag.includes('Compat')) return 'Legacy Compatibility'; |
| if (path.startsWith('/api/short-hunter')) return 'Short Hunter Gateway'; |
| if (path.startsWith('/api/providers') || path.startsWith('/api/resources') || path.startsWith('/api/pools') || path.startsWith('/api/logs') || path.startsWith('/api/diagnostics') || path.startsWith('/api/apl')) { |
| return 'Resources & Operations'; |
| } |
| if (path.startsWith('/api/models') || path.startsWith('/api/hf') || path.startsWith('/api/analyze') || path.startsWith('/api/ai') || path.startsWith('/api/trading/decision')) { |
| return 'AI & Models'; |
| } |
| if (path.startsWith('/api/news') || path.startsWith('/api/sentiment') || path.startsWith('/api/market') || path.startsWith('/api/trending') || path.startsWith('/api/coins') || path.startsWith('/api/ohlcv') || path.startsWith('/api/klines') || path.startsWith('/api/history') || path.startsWith('/api/orderbook') || path.startsWith('/api/indicators') || path.startsWith('/api/defi')) { |
| return 'Market Data & Signals'; |
| } |
| if (path === '/' || path.endsWith('.html') || path === '/health' || path === '/api/health' || path === '/api/status' || path === '/api/stats' || path === '/debug-info' || path === '/trading_pairs.txt') { |
| return 'Core App & UI'; |
| } |
| return 'Legacy Compatibility'; |
| } |
|
|
| function isJsonResponse(response) { |
| const content = response?.content || {}; |
| return Object.keys(content).some((key) => key.includes('json')); |
| } |
|
|
| function getRequestBody(operation, components) { |
| const body = operation?.requestBody?.content || {}; |
| const jsonContent = body['application/json'] || body['application/*+json'] || Object.values(body)[0]; |
| if (!jsonContent) return null; |
| const schema = jsonContent.schema || null; |
| const sample = sampleFromSchema(schema, components); |
| return { |
| description: operation.requestBody.description || '', |
| sample, |
| }; |
| } |
|
|
| function getResponses(operation, components) { |
| const responses = []; |
| Object.entries(operation?.responses || {}).forEach(([status, response]) => { |
| const content = response?.content || {}; |
| const jsonContent = content['application/json'] || content['application/*+json'] || Object.values(content).find((item) => item?.schema); |
| const schema = jsonContent?.schema || null; |
| responses.push({ |
| status, |
| description: response?.description || '', |
| schema, |
| sample: sampleFromSchema(schema, components), |
| }); |
| }); |
| return responses; |
| } |
|
|
| function getParameters(operation, components) { |
| return (operation?.parameters || []).map((param) => { |
| const schema = unwrapSchema(param.schema, components); |
| return { |
| name: param.name, |
| in: param.in, |
| required: !!param.required, |
| description: param.description || '', |
| type: schemaTypeLabel(schema || param.schema, components), |
| example: sampleFromSchema(schema || param.schema, components, param.name), |
| }; |
| }); |
| } |
|
|
| function buildQueryString(parameters) { |
| const query = new URLSearchParams(); |
| parameters.filter((item) => item.in === 'query').forEach((param) => { |
| if (param.example !== undefined && param.example !== null && param.example !== '') { |
| query.set(param.name, String(param.example)); |
| } |
| }); |
| const value = query.toString(); |
| return value ? `?${value}` : ''; |
| } |
|
|
| function replacePathParams(path, parameters) { |
| return path.replace(/\{([^}]+)\}/g, (_, key) => { |
| const param = parameters.find((item) => item.name === key); |
| if (param?.example !== undefined && param.example !== null && param.example !== '') { |
| return encodeURIComponent(String(param.example)); |
| } |
| return encodeURIComponent(key === 'symbol' ? 'BTCUSDT' : key); |
| }); |
| } |
|
|
| function operationSearchText(operation) { |
| const parameters = Array.isArray(operation.parameters) ? operation.parameters : []; |
| return [ |
| operation.method, |
| operation.path, |
| operation.summary, |
| operation.description, |
| operation.tags?.join(' '), |
| parameters.map((p) => `${p.name} ${p.description} ${p.type}`).join(' '), |
| JSON.stringify(operation.requestBody?.sample || {}), |
| JSON.stringify(operation.responses?.map((resp) => resp.sample || {}) || []), |
| ] |
| .join(' ') |
| .toLowerCase(); |
| } |
|
|
| function buildRouteDocs(spec) { |
| const components = spec?.components || {}; |
| const routes = []; |
| Object.entries(spec?.paths || {}).forEach(([path, pathItem]) => { |
| Object.entries(pathItem || {}).forEach(([method, operation]) => { |
| if (!operation || typeof operation !== 'object' || method === 'parameters') return; |
| const upper = method.toUpperCase(); |
| routes.push({ |
| method: upper, |
| path, |
| tags: operation.tags || [], |
| summary: operation.summary || toTitleCase(path.split('/').filter(Boolean).slice(-1)[0] || path), |
| description: operation.description || '', |
| deprecated: !!operation.deprecated, |
| parameters: getParameters(operation, components), |
| requestBody: getRequestBody(operation, components), |
| responses: getResponses(operation, components), |
| operationId: operation.operationId || '', |
| group: operationGroup(path, operation.tags || []), |
| }); |
| }); |
| }); |
|
|
| routes.sort((a, b) => { |
| if (a.group !== b.group) return a.group.localeCompare(b.group); |
| if (a.path !== b.path) return a.path.localeCompare(b.path); |
| return a.method.localeCompare(b.method); |
| }); |
|
|
| const groups = []; |
| const index = new Map(); |
| routes.forEach((route) => { |
| if (!index.has(route.group)) { |
| index.set(route.group, { name: route.group, routes: [] }); |
| groups.push(index.get(route.group)); |
| } |
| index.get(route.group).routes.push(route); |
| }); |
|
|
| const schemas = Object.entries(components.schemas || {}).map(([name, schema]) => ({ |
| name, |
| description: schema.description || '', |
| type: schemaTypeLabel(schema, components), |
| sample: sampleFromSchema(schema, components, name), |
| schema, |
| })); |
|
|
| return { routes, groups, schemas }; |
| } |
|
|
| function buildServiceCards() { |
| return ServiceManifest.map((service) => ` |
| <article class="help-service-card glass-card"> |
| <div class="help-service-title"> |
| <h4>${escapeHtml(service.name)}</h4> |
| <span class="badge badge-cyan">${escapeHtml(service.module)}</span> |
| </div> |
| <p class="text-secondary">${escapeHtml(service.role)}</p> |
| </article> |
| `).join(''); |
| } |
|
|
| function renderShell() { |
| if (!state.section) return; |
| ensureStyles(); |
| state.section.innerHTML = ` |
| <div class="help-page-shell"> |
| <div class="glass-card help-hero-card"> |
| <div class="help-hero-top"> |
| <div> |
| <div class="help-kicker">Live API reference</div> |
| <h3>Help / API Reference</h3> |
| <p class="text-secondary">This page reads the live OpenAPI schema, the Short Hunter gateway, and the legacy compatibility surface so the docs stay synchronized with the backend.</p> |
| </div> |
| <div class="help-hero-actions"> |
| <button class="btn-refresh help-action-btn" data-help-refresh type="button"> |
| ${icon.refresh} |
| Refresh docs |
| </button> |
| <button class="btn-refresh help-action-btn" data-help-copy-openapi type="button"> |
| ${icon.copy} |
| Copy OpenAPI URL |
| </button> |
| </div> |
| </div> |
| |
| <div class="help-stats-grid" data-help-stats> |
| <div class="help-stat-card"> |
| <span class="help-stat-label">Endpoints</span> |
| <strong data-help-endpoint-count>0</strong> |
| </div> |
| <div class="help-stat-card"> |
| <span class="help-stat-label">Schemas</span> |
| <strong data-help-schema-count>0</strong> |
| </div> |
| <div class="help-stat-card"> |
| <span class="help-stat-label">Providers</span> |
| <strong data-help-provider-count>0</strong> |
| </div> |
| <div class="help-stat-card"> |
| <span class="help-stat-label">Capabilities</span> |
| <strong data-help-capability-count>0</strong> |
| </div> |
| <div class="help-stat-card"> |
| <span class="help-stat-label">Gateway mode</span> |
| <strong data-help-gateway-mode>unknown</strong> |
| </div> |
| </div> |
| </div> |
| |
| <div class="help-toolbar glass-card"> |
| <label class="help-search" aria-label="Search endpoints and models"> |
| <span>${icon.search}</span> |
| <input type="search" placeholder="Search endpoints, models, params, or services" data-help-search /> |
| </label> |
| <div class="help-toolbar-meta"> |
| <span class="badge badge-success" data-help-openapi-status>OpenAPI pending</span> |
| <span class="badge badge-info" data-help-result-count>0 visible</span> |
| </div> |
| </div> |
| |
| <div class="help-layout"> |
| <aside class="help-nav glass-card"> |
| ${SectionIds.map((item) => ` |
| <button type="button" class="help-nav-link" data-help-jump="${item.id}"> |
| ${item.label} |
| </button> |
| `).join('')} |
| </aside> |
| |
| <div class="help-content"> |
| <section class="glass-card help-section" id="help-overview"> |
| <div class="help-section-head"> |
| <div> |
| <h3>Overview</h3> |
| <p class="text-secondary">What the system provides and how requests travel from the browser to the provider router and back.</p> |
| </div> |
| </div> |
| <div class="help-flow"> |
| <div class="help-flow-step">Frontend UI</div> |
| <div class="help-flow-arrow">${icon.link}</div> |
| <div class="help-flow-step">FastAPI backend</div> |
| <div class="help-flow-arrow">${icon.link}</div> |
| <div class="help-flow-step">Compat + Short Hunter routers</div> |
| <div class="help-flow-arrow">${icon.link}</div> |
| <div class="help-flow-step">Provider adapters, cache, and health scoring</div> |
| <div class="help-flow-arrow">${icon.link}</div> |
| <div class="help-flow-step">Normalized JSON response</div> |
| </div> |
| <div class="help-grid-2"> |
| <div> |
| <h4>Data the system exposes</h4> |
| <ul class="help-list"> |
| <li>Market data, OHLCV, order books, funding, open interest, sentiment, news, and model outputs.</li> |
| <li>Short Hunter normalized futures snapshots with explicit sourceMode, dataState, and noTradeGuard fields.</li> |
| <li>Provider and resource catalogs, health status, diagnostics, logs, and migration helpers.</li> |
| </ul> |
| </div> |
| <div> |
| <h4>Key modules</h4> |
| <ul class="help-list"> |
| <li><code>api_server_extended.py</code> provides the FastAPI app and legacy UI routes.</li> |
| <li><code>api_compat_routes.py</code> preserves older endpoints and aliases.</li> |
| <li><code>short_hunter_routes.py</code> exposes the gateway contract.</li> |
| <li><code>providers/router.py</code> handles rotation, fallback, cache, and cooldowns.</li> |
| </ul> |
| </div> |
| </div> |
| </section> |
| |
| <section class="glass-card help-section" id="help-services"> |
| <div class="help-section-head"> |
| <div> |
| <h3>Services and data fetchers</h3> |
| <p class="text-secondary">The backend stays honest about which upstreams it depends on and where those integrations live.</p> |
| </div> |
| </div> |
| <div class="help-card-grid" data-help-service-grid> |
| ${buildServiceCards()} |
| </div> |
| </section> |
| |
| <section class="glass-card help-section" id="help-apis"> |
| <div class="help-section-head"> |
| <div> |
| <h3>APIs</h3> |
| <p class="text-secondary">Expandable route cards with methods, parameters, request bodies, response schemas, and example calls.</p> |
| </div> |
| </div> |
| <div class="help-api-groups" data-help-api-groups> |
| <div class="help-empty-state">Loading routes from <code>/openapi.json</code>...</div> |
| </div> |
| </section> |
| |
| <section class="glass-card help-section" id="help-data-models"> |
| <div class="help-section-head"> |
| <div> |
| <h3>Data models</h3> |
| <p class="text-secondary">OpenAPI component schemas used by the backend.</p> |
| </div> |
| </div> |
| <div class="help-model-grid" data-help-model-grid> |
| <div class="help-empty-state">Loading schemas...</div> |
| </div> |
| </section> |
| |
| <section class="glass-card help-section" id="help-integration"> |
| <div class="help-section-head"> |
| <div> |
| <h3>Integration guide</h3> |
| <p class="text-secondary">How the current frontend calls the backend, which headers are actually needed, and how to wire a consumer safely.</p> |
| </div> |
| </div> |
| <div class="help-grid-2"> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Browser fetch</strong> |
| <button class="help-copy-btn" type="button" data-copy-target="fetch-example">${icon.copy}</button> |
| </div> |
| <pre id="fetch-example" class="help-code">const response = await fetch('/api/short-hunter/snapshot/BTCUSDT'); |
| const snapshot = await response.json();</pre> |
| </div> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Frontend wrapper</strong> |
| <button class="help-copy-btn" type="button" data-copy-target="wrapper-example">${icon.copy}</button> |
| </div> |
| <pre id="wrapper-example" class="help-code">import apiClient from './apiClient.js'; |
| const data = await apiClient.get('/api/providers/status');</pre> |
| </div> |
| </div> |
| <div class="help-grid-2"> |
| <div class="help-note-card"> |
| <h4>Headers</h4> |
| <p class="text-secondary">The inspected frontend does not require a user auth header for these routes. Provider secrets stay server-side through environment variables and the backend reads them internally.</p> |
| </div> |
| <div class="help-note-card"> |
| <h4>WebSockets</h4> |
| <p class="text-secondary">The repository includes client-side WebSocket helpers, but no server-side <code>@app.websocket</code> route is exposed in the inspected FastAPI app. Mark real-time usage as undocumented unless a websocket route is added later.</p> |
| </div> |
| </div> |
| <div class="help-grid-2"> |
| <div class="help-note-card"> |
| <h4>State management</h4> |
| <p class="text-secondary">The main UI keeps navigation and refresh behavior in <code>static/js/app.js</code> and tab-specific loaders. Consumers should mirror that pattern: fetch, normalize, and then render from the returned JSON.</p> |
| </div> |
| <div class="help-note-card"> |
| <h4>Error handling</h4> |
| <p class="text-secondary">Treat <code>success: false</code>, <code>sourceMode: UNAVAILABLE</code>, <code>dataState: UNAVAILABLE</code>, and <code>noTradeGuard: true</code> as truthful signals that the backend could not verify the data.</p> |
| </div> |
| </div> |
| </section> |
| |
| <section class="glass-card help-section" id="help-examples"> |
| <div class="help-section-head"> |
| <div> |
| <h3>Examples</h3> |
| <p class="text-secondary">Real endpoints with generated request and response previews.</p> |
| </div> |
| </div> |
| <div class="help-example-grid" data-help-example-grid> |
| <div class="help-empty-state">Loading examples...</div> |
| </div> |
| </section> |
| </div> |
| </div> |
| </div> |
| `; |
|
|
| state.sectionReady = true; |
| bindShellEvents(); |
| } |
|
|
| function bindShellEvents() { |
| if (!state.section) return; |
| const refreshBtn = state.section.querySelector('[data-help-refresh]'); |
| refreshBtn?.addEventListener('click', () => refresh()); |
|
|
| const copyOpenApi = state.section.querySelector('[data-help-copy-openapi]'); |
| copyOpenApi?.addEventListener('click', () => copyText(`${window.location.origin}/openapi.json`)); |
|
|
| const searchInput = state.section.querySelector('[data-help-search]'); |
| searchInput?.addEventListener('input', () => { |
| state.search = searchInput.value.trim().toLowerCase(); |
| applySearch(); |
| }); |
|
|
| state.section.querySelectorAll('[data-help-jump]').forEach((button) => { |
| button.addEventListener('click', () => { |
| const id = button.dataset.helpJump; |
| const target = state.section.querySelector(`#help-${id}`); |
| target?.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| }); |
| }); |
| } |
|
|
| async function copyText(text) { |
| try { |
| await navigator.clipboard.writeText(text); |
| toast('success', 'Copied to clipboard'); |
| } catch (error) { |
| toast('warning', `Copy failed: ${error.message}`); |
| } |
| } |
|
|
| function renderStats() { |
| if (!state.section) return; |
| const endpointCount = state.routes.length; |
| const schemaCount = state.schemas.length; |
| const providerCount = state.providerCount; |
| const capabilityCount = state.capabilityCount; |
| const mode = state.spec ? 'synced' : 'fallback'; |
| const endpointNode = state.section.querySelector('[data-help-endpoint-count]'); |
| const schemaNode = state.section.querySelector('[data-help-schema-count]'); |
| const providerNode = state.section.querySelector('[data-help-provider-count]'); |
| const capabilityNode = state.section.querySelector('[data-help-capability-count]'); |
| const modeNode = state.section.querySelector('[data-help-gateway-mode]'); |
| const statusNode = state.section.querySelector('[data-help-openapi-status]'); |
| const resultNode = state.section.querySelector('[data-help-result-count]'); |
|
|
| if (endpointNode) endpointNode.textContent = String(endpointCount); |
| if (schemaNode) schemaNode.textContent = String(schemaCount); |
| if (providerNode) providerNode.textContent = String(providerCount); |
| if (capabilityNode) capabilityNode.textContent = String(capabilityCount); |
| if (modeNode) modeNode.textContent = mode; |
| if (statusNode) statusNode.textContent = state.spec ? 'OpenAPI synced' : 'Fallback catalog'; |
| if (resultNode) resultNode.textContent = `${visibleRouteCount()} visible`; |
| } |
|
|
| function visibleRouteCount() { |
| return state.section ? state.section.querySelectorAll('[data-route-card]:not(.is-hidden)').length : 0; |
| } |
|
|
| function renderApiGroups() { |
| const container = state.section?.querySelector('[data-help-api-groups]'); |
| if (!container) return; |
|
|
| if (!state.routes.length) { |
| container.innerHTML = '<div class="help-empty-state">No routes available.</div>'; |
| return; |
| } |
|
|
| container.innerHTML = state.groups.map((group, index) => ` |
| <details class="help-route-group" ${index === 0 ? 'open' : ''} data-route-group data-group-name="${escapeHtml(group.name)}"> |
| <summary> |
| <span>${escapeHtml(group.name)}</span> |
| <span class="badge badge-info" data-group-count>${group.routes.length}</span> |
| </summary> |
| <div class="help-route-list"> |
| ${group.routes.map(renderRouteCard).join('')} |
| </div> |
| </details> |
| `).join(''); |
|
|
| container.querySelectorAll('[data-copy-route]').forEach((button) => { |
| button.addEventListener('click', () => { |
| const text = button.dataset.copyRoute || ''; |
| copyText(text); |
| }); |
| }); |
|
|
| container.querySelectorAll('[data-copy-payload]').forEach((button) => { |
| button.addEventListener('click', () => { |
| const text = button.dataset.copyPayload || ''; |
| copyText(text); |
| }); |
| }); |
| } |
|
|
| function renderRouteCard(route) { |
| const params = route.parameters || []; |
| const queryString = buildQueryString(params); |
| const examplePath = replacePathParams(route.path, params); |
| const exampleUrl = `${examplePath}${queryString}`; |
| const requestBody = route.requestBody?.sample !== undefined && route.requestBody?.sample !== null |
| ? JSON.stringify(route.requestBody.sample, null, 2) |
| : ''; |
| const response = route.responses.find((item) => String(item.status).startsWith('2')) || route.responses[0] || null; |
| const responseBody = response?.sample !== undefined && response?.sample !== null ? JSON.stringify(response.sample, null, 2) : ''; |
| const responseLabel = response ? `${response.status} ${response.description || ''}`.trim() : 'undocumented'; |
| const methodClass = `badge-${methodTone[route.method] || 'info'}`; |
| const searchText = operationSearchText(route).replace(/"/g, '"'); |
|
|
| return ` |
| <details class="help-route-card" data-route-card data-search-text="${searchText}"> |
| <summary> |
| <div class="help-route-summary"> |
| <span class="badge ${methodClass}">${escapeHtml(route.method)}</span> |
| <span class="help-route-path">${escapeHtml(route.path)}</span> |
| </div> |
| <div class="help-route-summary-meta"> |
| <span>${escapeHtml(route.summary)}</span> |
| ${route.deprecated ? '<span class="badge badge-warning">deprecated</span>' : ''} |
| </div> |
| </summary> |
| |
| <div class="help-route-body"> |
| ${route.description ? `<p class="help-route-description">${escapeHtml(route.description)}</p>` : ''} |
| <div class="help-copy-row"> |
| <button type="button" class="help-copy-btn" data-copy-route="${escapeHtml(exampleUrl)}">${icon.copy} Copy endpoint</button> |
| ${requestBody ? `<button type="button" class="help-copy-btn" data-copy-payload="${escapeHtml(requestBody)}">${icon.copy} Copy payload</button>` : ''} |
| </div> |
| |
| <div class="help-grid-2"> |
| <div> |
| <h5>Parameters</h5> |
| ${params.length ? ` |
| <div class="help-params"> |
| ${params.map((param) => ` |
| <div class="help-param-row"> |
| <div> |
| <strong>${escapeHtml(param.name)}</strong> |
| <span class="badge badge-cyan">${escapeHtml(param.in)}</span> |
| ${param.required ? '<span class="badge badge-warning">required</span>' : '<span class="badge badge-info">optional</span>'} |
| </div> |
| <div class="help-param-meta">${escapeHtml(param.type)}</div> |
| <p class="help-muted">${escapeHtml(param.description || 'undocumented')}</p> |
| <code class="help-inline-code">${escapeHtml(String(param.example))}</code> |
| </div> |
| `).join('')} |
| </div> |
| ` : '<p class="help-muted">No parameters documented in OpenAPI.</p>'} |
| </div> |
| |
| <div> |
| <h5>Response preview</h5> |
| <p class="help-muted">${escapeHtml(responseLabel)}</p> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>JSON</strong> |
| ${responseBody ? `<button class="help-copy-btn" type="button" data-copy-payload="${escapeHtml(responseBody)}">${icon.copy}</button>` : ''} |
| </div> |
| <pre class="help-code">${responseBody ? escapeHtml(responseBody) : 'undocumented'}</pre> |
| </div> |
| </div> |
| </div> |
| |
| <div class="help-grid-2"> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Example request</strong> |
| <button class="help-copy-btn" type="button" data-copy-payload="${escapeHtml(`curl -X ${route.method} "${window.location.origin}${exampleUrl}"`)}">${icon.copy}</button> |
| </div> |
| <pre class="help-code">curl -X ${route.method} "${window.location.origin}${exampleUrl}"</pre> |
| </div> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Request body</strong> |
| ${requestBody ? `<button class="help-copy-btn" type="button" data-copy-payload="${escapeHtml(requestBody)}">${icon.copy}</button>` : ''} |
| </div> |
| <pre class="help-code">${requestBody ? escapeHtml(requestBody) : 'No request body documented.'}</pre> |
| </div> |
| </div> |
| </div> |
| </details> |
| `; |
| } |
|
|
| function renderModels() { |
| const container = state.section?.querySelector('[data-help-model-grid]'); |
| if (!container) return; |
|
|
| const models = state.schemas.length ? state.schemas : FallbackSchemas; |
| container.innerHTML = models.map((schema) => { |
| const sample = JSON.stringify(schema.sample, null, 2); |
| const searchText = `${schema.name} ${schema.description} ${schema.type} ${sample}`.toLowerCase().replace(/"/g, '"'); |
| return ` |
| <article class="help-model-card glass-card" data-schema-card data-search-text="${searchText}"> |
| <div class="help-model-head"> |
| <div> |
| <h4>${escapeHtml(schema.name)}</h4> |
| <p class="help-muted">${escapeHtml(schema.description || 'undocumented')}</p> |
| </div> |
| <span class="badge badge-info">${escapeHtml(schema.type || 'object')}</span> |
| </div> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Sample</strong> |
| <button type="button" class="help-copy-btn" data-copy-payload="${escapeHtml(sample)}">${icon.copy}</button> |
| </div> |
| <pre class="help-code">${escapeHtml(sample)}</pre> |
| </div> |
| </article> |
| `; |
| }).join(''); |
|
|
| container.querySelectorAll('[data-copy-payload]').forEach((button) => { |
| button.addEventListener('click', () => copyText(button.dataset.copyPayload || '')); |
| }); |
| } |
|
|
| function renderExamples() { |
| const container = state.section?.querySelector('[data-help-example-grid]'); |
| if (!container) return; |
|
|
| const examples = [ |
| { |
| title: 'Short Hunter snapshot', |
| method: 'GET', |
| path: '/api/short-hunter/snapshot/BTCUSDT', |
| purpose: 'Normalized futures snapshot with noTradeGuard and provider attempt metadata.', |
| responseHint: { |
| success: true, |
| sourceMode: 'LIVE', |
| dataState: 'REAL', |
| data: { ticker: {}, ohlcv: [], orderbook: {}, funding: {}, openInterest: {} }, |
| }, |
| }, |
| { |
| title: 'Gateway provider status', |
| method: 'GET', |
| path: '/api/short-hunter/providers/status', |
| purpose: 'Capability-by-capability provider status, cooldown, and latency summary.', |
| responseHint: { |
| providers: [], |
| summary: { healthy: 0, degraded: 0, disabled: 0 }, |
| }, |
| }, |
| { |
| title: 'Sentiment analysis', |
| method: 'POST', |
| path: '/api/sentiment/analyze', |
| purpose: 'Server-side text sentiment and crypto context analysis.', |
| requestHint: { text: 'Bitcoin breaks resistance', mode: 'auto' }, |
| responseHint: { available: true, sentiment: 'POSITIVE', confidence: 0.84 }, |
| }, |
| { |
| title: 'Legacy market history', |
| method: 'GET', |
| path: '/api/history?symbol=BTCUSDT&interval=1h&limit=100', |
| purpose: 'Compatibility wrapper for historical candles.', |
| responseHint: { data: [], source: 'compat' }, |
| }, |
| ]; |
|
|
| container.innerHTML = examples.map((example) => { |
| const request = example.requestHint ? JSON.stringify(example.requestHint, null, 2) : `curl -X ${example.method} "${window.location.origin}${example.path}"`; |
| const response = JSON.stringify(example.responseHint, null, 2); |
| const searchText = `${example.title} ${example.method} ${example.path} ${example.purpose} ${request} ${response}`.toLowerCase().replace(/"/g, '"'); |
| return ` |
| <article class="help-example-card glass-card" data-example-card data-search-text="${searchText}"> |
| <div class="help-example-head"> |
| <div> |
| <div class="help-example-badge"> |
| <span class="badge ${methodTone[example.method] ? `badge-${methodTone[example.method]}` : 'badge-info'}">${escapeHtml(example.method)}</span> |
| <code>${escapeHtml(example.path)}</code> |
| </div> |
| <h4>${escapeHtml(example.title)}</h4> |
| <p class="help-muted">${escapeHtml(example.purpose)}</p> |
| </div> |
| <button type="button" class="help-copy-btn" data-copy-payload="${escapeHtml(request)}">${icon.copy}</button> |
| </div> |
| <div class="help-grid-2"> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Request</strong> |
| <button type="button" class="help-copy-btn" data-copy-payload="${escapeHtml(request)}">${icon.copy}</button> |
| </div> |
| <pre class="help-code">${escapeHtml(request)}</pre> |
| </div> |
| <div class="help-code-block"> |
| <div class="help-code-head"> |
| <strong>Response</strong> |
| <button type="button" class="help-copy-btn" data-copy-payload="${escapeHtml(response)}">${icon.copy}</button> |
| </div> |
| <pre class="help-code">${escapeHtml(response)}</pre> |
| </div> |
| </div> |
| </article> |
| `; |
| }).join(''); |
|
|
| container.querySelectorAll('[data-copy-payload]').forEach((button) => { |
| button.addEventListener('click', () => copyText(button.dataset.copyPayload || '')); |
| }); |
| } |
|
|
| function applySearch() { |
| if (!state.section) return; |
| const term = state.search.trim(); |
| const lower = term.toLowerCase(); |
| const routeCards = state.section.querySelectorAll('[data-route-card]'); |
| const schemaCards = state.section.querySelectorAll('[data-schema-card]'); |
| const exampleCards = state.section.querySelectorAll('[data-example-card]'); |
| const groups = state.section.querySelectorAll('[data-route-group]'); |
|
|
| let visibleCount = 0; |
| routeCards.forEach((card) => { |
| const text = (card.dataset.searchText || '').toLowerCase(); |
| const match = !lower || text.includes(lower); |
| card.classList.toggle('is-hidden', !match); |
| if (match) visibleCount += 1; |
| }); |
|
|
| schemaCards.forEach((card) => { |
| const text = (card.dataset.searchText || '').toLowerCase(); |
| const match = !lower || text.includes(lower); |
| card.classList.toggle('is-hidden', !match); |
| }); |
|
|
| exampleCards.forEach((card) => { |
| const text = (card.dataset.searchText || '').toLowerCase(); |
| const match = !lower || text.includes(lower); |
| card.classList.toggle('is-hidden', !match); |
| }); |
|
|
| groups.forEach((group) => { |
| const visibleChildren = group.querySelectorAll('[data-route-card]:not(.is-hidden)').length; |
| group.classList.toggle('is-hidden', visibleChildren === 0); |
| const count = group.querySelector('[data-group-count]'); |
| if (count) count.textContent = String(visibleChildren); |
| }); |
|
|
| const resultNode = state.section.querySelector('[data-help-result-count]'); |
| if (resultNode) { |
| resultNode.textContent = `${visibleCount} visible`; |
| } |
| } |
|
|
| function renderFallbackMessage(message) { |
| const groups = state.section?.querySelector('[data-help-api-groups]'); |
| const models = state.section?.querySelector('[data-help-model-grid]'); |
| const examples = state.section?.querySelector('[data-help-example-grid]'); |
| if (groups) { |
| groups.innerHTML = `<div class="help-empty-state">${escapeHtml(message)}</div>`; |
| } |
| if (models) { |
| models.innerHTML = `<div class="help-empty-state">${escapeHtml(message)}</div>`; |
| } |
| if (examples) { |
| examples.innerHTML = `<div class="help-empty-state">${escapeHtml(message)}</div>`; |
| } |
| } |
|
|
| async function loadDocs(force = false) { |
| if (state.loading && !force) return state.loadPromise; |
| state.loading = true; |
|
|
| renderShell(); |
| renderFallbackMessage('Loading live route catalog...'); |
|
|
| state.loadPromise = (async () => { |
| try { |
| const [openapiRes, providersRes, shortHunterRes, shortHunterCapsRes, resourceRes] = await Promise.allSettled([ |
| fetch('/openapi.json', { cache: 'no-store' }), |
| fetch('/api/providers/status', { cache: 'no-store' }), |
| fetch('/api/short-hunter/providers/status', { cache: 'no-store' }), |
| fetch('/api/short-hunter/capabilities', { cache: 'no-store' }), |
| fetch('/api/resources/apis/raw', { cache: 'no-store' }), |
| ]); |
|
|
| const providersJson = providersRes.status === 'fulfilled' && providersRes.value.ok ? await providersRes.value.json() : null; |
| const shortHunterJson = shortHunterRes.status === 'fulfilled' && shortHunterRes.value.ok ? await shortHunterRes.value.json() : null; |
| const capsJson = shortHunterCapsRes.status === 'fulfilled' && shortHunterCapsRes.value.ok ? await shortHunterCapsRes.value.json() : null; |
| const resourceJson = resourceRes.status === 'fulfilled' && resourceRes.value.ok ? await resourceRes.value.json() : null; |
|
|
| const providersList = providersJson?.providers || providersJson?.data?.providers || []; |
| const shortHunterProviders = shortHunterJson?.providers || []; |
| state.capabilityCount = capsJson?.capabilities ? Object.keys(capsJson.capabilities).length : 0; |
| state.services = [ |
| ...providersList, |
| ...shortHunterProviders, |
| ...(resourceJson?.files || []), |
| ]; |
| state.providerCount = providersList.length + shortHunterProviders.length; |
|
|
| if (openapiRes.status === 'fulfilled' && openapiRes.value.ok) { |
| state.spec = await openapiRes.value.json(); |
| const routeDocs = buildRouteDocs(state.spec); |
| state.routes = routeDocs.routes.length ? routeDocs.routes : FallbackRoutes; |
| state.groups = routeDocs.groups.length ? routeDocs.groups : groupFallbackRoutes(FallbackRoutes); |
| state.schemas = routeDocs.schemas.length ? routeDocs.schemas : FallbackSchemas; |
| } else { |
| state.spec = null; |
| state.routes = FallbackRoutes; |
| state.groups = groupFallbackRoutes(FallbackRoutes); |
| state.schemas = FallbackSchemas; |
| toast('warning', 'OpenAPI schema not available; using a compact fallback catalog.'); |
| } |
|
|
| renderShell(); |
| renderStats(); |
| renderApiGroups(); |
| renderModels(); |
| renderExamples(); |
| applySearch(); |
| wireCopyButtons(); |
| updateOpenApiStatus(); |
| state.loaded = true; |
| } catch (error) { |
| console.error('Help reference load failed:', error); |
| state.spec = null; |
| state.routes = FallbackRoutes; |
| state.groups = groupFallbackRoutes(FallbackRoutes); |
| state.schemas = FallbackSchemas; |
| renderShell(); |
| renderStats(); |
| renderApiGroups(); |
| renderModels(); |
| renderExamples(); |
| applySearch(); |
| wireCopyButtons(); |
| updateOpenApiStatus(true, error.message); |
| toast('warning', `Help reference loaded from fallback data: ${error.message}`); |
| } finally { |
| state.loading = false; |
| } |
| })(); |
|
|
| return state.loadPromise; |
| } |
|
|
| function groupFallbackRoutes(routes) { |
| const grouped = new Map(); |
| routes.forEach((route) => { |
| const key = route.group || operationGroup(route.path, []); |
| if (!grouped.has(key)) grouped.set(key, { name: key, routes: [] }); |
| grouped.get(key).routes.push({ |
| ...route, |
| tags: route.tags || [], |
| description: route.description || '', |
| parameters: route.parameters || [], |
| requestBody: route.requestBody || null, |
| responses: route.responses || [], |
| }); |
| }); |
| return Array.from(grouped.values()); |
| } |
|
|
| function wireCopyButtons() { |
| if (!state.section) return; |
| state.section.querySelectorAll('[data-copy-route]').forEach((button) => { |
| if (button.dataset.bound === '1') return; |
| button.dataset.bound = '1'; |
| button.addEventListener('click', () => copyText(button.dataset.copyRoute || '')); |
| }); |
| state.section.querySelectorAll('[data-copy-payload]').forEach((button) => { |
| if (button.dataset.bound === '1') return; |
| button.dataset.bound = '1'; |
| button.addEventListener('click', () => copyText(button.dataset.copyPayload || '')); |
| }); |
| state.section.querySelectorAll('[data-help-service-grid] [data-copy-payload]').forEach((button) => { |
| if (button.dataset.bound === '1') return; |
| button.dataset.bound = '1'; |
| button.addEventListener('click', () => copyText(button.dataset.copyPayload || '')); |
| }); |
| } |
|
|
| function updateOpenApiStatus(isFallback = false, message = '') { |
| if (!state.section) return; |
| const status = state.section.querySelector('[data-help-openapi-status]'); |
| if (!status) return; |
| if (state.spec) { |
| status.textContent = 'OpenAPI synced'; |
| status.className = 'badge badge-success'; |
| return; |
| } |
| status.textContent = isFallback ? 'Fallback catalog' : 'OpenAPI unavailable'; |
| status.className = isFallback ? 'badge badge-warning' : 'badge badge-danger'; |
| if (message) status.title = message; |
| } |
|
|
| async function mount(section, options = {}) { |
| if (!section) return; |
| state.section = section; |
| if (state.loaded && !options.refresh && state.sectionReady) { |
| renderStats(); |
| applySearch(); |
| updateOpenApiStatus(); |
| return; |
| } |
| if (!state.sectionReady) { |
| await loadDocs(!!options.refresh); |
| } else if (options.refresh) { |
| await loadDocs(true); |
| } |
| } |
|
|
| async function refresh() { |
| await loadDocs(true); |
| } |
|
|
| window.HelpReferenceView = { |
| mount, |
| refresh, |
| }; |
|
|
| window.loadHelpReference = async function loadHelpReference() { |
| const section = document.getElementById('tab-help'); |
| if (!section) return; |
| await mount(section); |
| }; |
| })(); |
|
|