(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: '', copy: '', search: '', link: '', }; 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, '''); } 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) => `

${escapeHtml(service.name)}

${escapeHtml(service.module)}

${escapeHtml(service.role)}

`).join(''); } function renderShell() { if (!state.section) return; ensureStyles(); state.section.innerHTML = `
Live API reference

Help / API Reference

This page reads the live OpenAPI schema, the Short Hunter gateway, and the legacy compatibility surface so the docs stay synchronized with the backend.

Endpoints 0
Schemas 0
Providers 0
Capabilities 0
Gateway mode unknown
OpenAPI pending 0 visible

Overview

What the system provides and how requests travel from the browser to the provider router and back.

Frontend UI
${icon.link}
FastAPI backend
${icon.link}
Compat + Short Hunter routers
${icon.link}
Provider adapters, cache, and health scoring
${icon.link}
Normalized JSON response

Data the system exposes

  • Market data, OHLCV, order books, funding, open interest, sentiment, news, and model outputs.
  • Short Hunter normalized futures snapshots with explicit sourceMode, dataState, and noTradeGuard fields.
  • Provider and resource catalogs, health status, diagnostics, logs, and migration helpers.

Key modules

  • api_server_extended.py provides the FastAPI app and legacy UI routes.
  • api_compat_routes.py preserves older endpoints and aliases.
  • short_hunter_routes.py exposes the gateway contract.
  • providers/router.py handles rotation, fallback, cache, and cooldowns.

Services and data fetchers

The backend stays honest about which upstreams it depends on and where those integrations live.

${buildServiceCards()}

APIs

Expandable route cards with methods, parameters, request bodies, response schemas, and example calls.

Loading routes from /openapi.json...

Data models

OpenAPI component schemas used by the backend.

Loading schemas...

Integration guide

How the current frontend calls the backend, which headers are actually needed, and how to wire a consumer safely.

Browser fetch
const response = await fetch('/api/short-hunter/snapshot/BTCUSDT');
const snapshot = await response.json();
Frontend wrapper
import apiClient from './apiClient.js';
const data = await apiClient.get('/api/providers/status');

Headers

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.

WebSockets

The repository includes client-side WebSocket helpers, but no server-side @app.websocket route is exposed in the inspected FastAPI app. Mark real-time usage as undocumented unless a websocket route is added later.

State management

The main UI keeps navigation and refresh behavior in static/js/app.js and tab-specific loaders. Consumers should mirror that pattern: fetch, normalize, and then render from the returned JSON.

Error handling

Treat success: false, sourceMode: UNAVAILABLE, dataState: UNAVAILABLE, and noTradeGuard: true as truthful signals that the backend could not verify the data.

Examples

Real endpoints with generated request and response previews.

Loading examples...
`; 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 = '
No routes available.
'; return; } container.innerHTML = state.groups.map((group, index) => `
${escapeHtml(group.name)} ${group.routes.length}
${group.routes.map(renderRouteCard).join('')}
`).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 `
${escapeHtml(route.method)} ${escapeHtml(route.path)}
${escapeHtml(route.summary)} ${route.deprecated ? 'deprecated' : ''}
${route.description ? `

${escapeHtml(route.description)}

` : ''}
${requestBody ? `` : ''}
Parameters
${params.length ? `
${params.map((param) => `
${escapeHtml(param.name)} ${escapeHtml(param.in)} ${param.required ? 'required' : 'optional'}
${escapeHtml(param.type)}

${escapeHtml(param.description || 'undocumented')}

${escapeHtml(String(param.example))}
`).join('')}
` : '

No parameters documented in OpenAPI.

'}
Response preview

${escapeHtml(responseLabel)}

JSON ${responseBody ? `` : ''}
${responseBody ? escapeHtml(responseBody) : 'undocumented'}
Example request
curl -X ${route.method} "${window.location.origin}${exampleUrl}"
Request body ${requestBody ? `` : ''}
${requestBody ? escapeHtml(requestBody) : 'No request body documented.'}
`; } 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 `

${escapeHtml(schema.name)}

${escapeHtml(schema.description || 'undocumented')}

${escapeHtml(schema.type || 'object')}
Sample
${escapeHtml(sample)}
`; }).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 `
${escapeHtml(example.method)} ${escapeHtml(example.path)}

${escapeHtml(example.title)}

${escapeHtml(example.purpose)}

Request
${escapeHtml(request)}
Response
${escapeHtml(response)}
`; }).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 = `
${escapeHtml(message)}
`; } if (models) { models.innerHTML = `
${escapeHtml(message)}
`; } if (examples) { examples.innerHTML = `
${escapeHtml(message)}
`; } } 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); }; })();