Really-amin's picture
Add self-contained styling fallback to Help page
db4335a verified
Raw
History Blame Contribute Delete
64.1 kB
(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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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, '&quot;');
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, '&quot;');
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, '&quot;');
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);
};
})();