Really-amin commited on
Commit
76dcb98
·
verified ·
1 Parent(s): ce3ff09

Upload 299 files

Browse files
Files changed (3) hide show
  1. CURSOR_UPDATE_PROMPT.md +741 -0
  2. api_server_extended.py +228 -49
  3. static/js/app.js +335 -64
CURSOR_UPDATE_PROMPT.md ADDED
@@ -0,0 +1,741 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Cursor Agent Prompt – UPDATE ONLY (no rewrite!)
2
+
3
+ Fix HF models, complete providers/resources, and integrate JSON registries into UI
4
+
5
+ You are working on an existing project called **Crypto Intelligence Hub** (the `final/` project the user uploaded).
6
+
7
+ It is already a full FastAPI + HTML/JS dashboard designed to run as a **Hugging Face Docker Space**.
8
+
9
+ > 🚨 CRITICAL WARNING – THIS IS AN UPDATE, NOT A REWRITE
10
+ >
11
+ > * Do **NOT** rewrite the architecture.
12
+ > * Do **NOT** introduce new frameworks.
13
+ > * Do **NOT** delete or replace major modules.
14
+ > * Do **NOT** simplify logic by removing features.
15
+ > Your job is to **fix, complete, and wire up** what is already there, and **leverage the new JSON files** the user added.
16
+
17
+ ---
18
+
19
+ ## 1. Existing structure (do not change high-level architecture)
20
+
21
+ Repository root contains (focus on these, do not move them):
22
+
23
+ * `final/hf_unified_server.py`
24
+
25
+ * ASGI entry point for the Docker Space:
26
+
27
+ ```python
28
+ from api_server_extended import app
29
+ ```
30
+
31
+ * `final/api_server_extended.py`
32
+
33
+ * Main FastAPI application:
34
+
35
+ * Endpoints:
36
+
37
+ * `/api/market`, `/api/trending`, `/api/news`, `/api/sentiment`
38
+
39
+ * `/api/providers`, `/api/resources`, `/api/models/*`
40
+
41
+ * `/api/diagnostics/*`, `/api/endpoints`, `/api/logs/*`, `/api/pools`
42
+
43
+ * `/health`, `/api/status`, etc.
44
+
45
+ * SQLite DB at `data/database/crypto_monitor.db`:
46
+
47
+ * `prices`
48
+
49
+ * `sentiment_analysis`
50
+
51
+ * `news_articles`
52
+
53
+ * Mounts static files and HTML templates.
54
+
55
+ * Defines `WORKSPACE_ROOT` and config paths.
56
+
57
+ * `final/ai_models.py`
58
+
59
+ * Hugging Face model registry and pipeline manager:
60
+
61
+ * Uses env: `HF_MODE`, `HF_TOKEN`, etc.
62
+
63
+ * Holds `MODEL_SPECS`, logical groups:
64
+
65
+ * `CRYPTO_SENTIMENT_MODELS`
66
+
67
+ * `FINANCIAL_SENTIMENT_MODELS`
68
+
69
+ * `SOCIAL_SENTIMENT_MODELS`
70
+
71
+ * `NEWS_SENTIMENT_MODELS`
72
+
73
+ * Provides `ModelRegistry`, `initialize_models()`, `get_pipeline()`, `ensemble_crypto_sentiment(...)`, etc.
74
+
75
+ * `final/config.py`
76
+
77
+ * Declares base HF model IDs in `HUGGINGFACE_MODELS`, including:
78
+
79
+ * `"crypto_sentiment": "ElKulako/cryptobert"`
80
+
81
+ * `"sentiment_financial": "ProsusAI/finbert"`
82
+
83
+ * Other sentiment/summarization models.
84
+
85
+ * `final/templates/index.html`
86
+
87
+ * `final/templates/unified_dashboard.html`
88
+
89
+ * Main "Ultimate" dashboard HTML:
90
+
91
+ * Tabs: Market, Monitor, Admin, HF, Pools, Logs, Resources, Reports, Advanced.
92
+
93
+ * Dark/glass style visual layout.
94
+
95
+ * `final/static/js/app.js`
96
+
97
+ * Main front-end logic:
98
+
99
+ * Tab switching: `switchTab`, `loadTabData`.
100
+
101
+ * Data loaders: `loadMarketData`, `loadModels`, `loadSentimentModels`, `loadSentimentHistory`, `loadNews`, `loadProviders`, `loadDiagnostics`, `loadAPIEndpoints`, etc.
102
+
103
+ * Calls backend endpoints under `/api/...`.
104
+
105
+ * `final/static/css/main.css`
106
+
107
+ * Main CSS for dashboard styling.
108
+
109
+ * `final/api-resources/`
110
+
111
+ * Contains JSON resource files (but some code paths still expect other JSON names/locations).
112
+
113
+ You must work within this architecture. Do not introduce React/Vue, do not move to a new server entry, etc.
114
+
115
+ ---
116
+
117
+ ## 2. New JSON files that MUST be used
118
+
119
+ The user has added **three important JSON files** that you must integrate into the backend and UI:
120
+
121
+ 1. `providers_config_extended.json`
122
+
123
+ * Extended registry of providers (CoinGecko, CoinPaprika, explorers, DeFi, etc.).
124
+
125
+ * Contains detailed metadata: `category`, `base_url`, `endpoints`, `rate_limit`, `requires_auth`, `priority`, `weight`, etc.
126
+
127
+ 2. `PROVIDER_AUTO_DISCOVERY_REPORT.json`
128
+
129
+ * Auto-discovery validation report:
130
+
131
+ * Contains:
132
+
133
+ * `stats`: counts of HTTP/HF providers, valid/invalid/conditional, execution time, etc.
134
+
135
+ * `http_providers.results[]`: each with:
136
+
137
+ * `provider_id`, `provider_name`, `provider_type`, `category`
138
+
139
+ * `status` (`VALID`, `INVALID`, `CONDITIONALLY_AVAILABLE`)
140
+
141
+ * `requires_auth`, `auth_env_var`, `error_reason`, `test_endpoint`, `response_time_ms`, `response_sample`.
142
+
143
+ * This is a **runtime validation snapshot** that must be exposed in the UI (e.g., as a diagnostics/providers health section).
144
+
145
+ 3. `all_apis_merged_2025.json`
146
+
147
+ * Master registry of APIs and keys:
148
+
149
+ * `metadata`: name, version (`2025.11.11`), description, created_at, source_files.
150
+
151
+ * `raw_files[]`: with big config text, free CORS proxies, RPC nodes, explorers, market data APIs, etc.
152
+
153
+ * This should back **API Explorer / Resources UI**, showing:
154
+
155
+ * Categories (market data, explorers, RPC, etc.).
156
+
157
+ * Example endpoints and usage.
158
+
159
+ * Possibly mention of CORS proxy patterns.
160
+
161
+ > You must **explicitly use these three files** in backend endpoints and update the UI to surface this information in a structured, useful way.
162
+
163
+ ---
164
+
165
+ ## 3. Known problems you must fix (without rewriting)
166
+
167
+ ### 3.1 HF models: 401, invalid IDs, 0 loaded pipelines
168
+
169
+ Logs show:
170
+
171
+ * For `ElKulako/cryptobert` and `ProsusAI/finbert`:
172
+
173
+ * 401 / Repository Not Found.
174
+
175
+ * Expired user access token `DreammakerCryptoSignalAndTrader2`.
176
+
177
+ * Registry ends up with:
178
+
179
+ ```text
180
+ 'models_loaded': 0, 'failed': [...]
181
+ ```
182
+
183
+ But `HF_MODE` gets reported as `'public'` or `'partial'`.
184
+
185
+ Problem:
186
+
187
+ * Some configured models are private/gated/not accessible with current token.
188
+
189
+ * Token is expired.
190
+
191
+ * The registry says "partial/public" but in practice, **no usable pipelines** exist.
192
+
193
+ ### 3.2 Providers config path & resources mismatch
194
+
195
+ In `api_server_extended.py`:
196
+
197
+ ```python
198
+ PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
199
+ ```
200
+
201
+ Previously, this file did not exist in the project – causing `/api/providers` to return an empty/minimal list.
202
+
203
+ Also, resources endpoints reference JSON filenames like:
204
+
205
+ * `crypto_resources_unified_2025-11-11.json`
206
+
207
+ * `all_apis_merged_2025.json` (or similar)
208
+
209
+ But the actual files and paths in the `final/` project were not aligned.
210
+
211
+ ### 3.3 Placeholders and half-implemented endpoints
212
+
213
+ * `/api/pools` returns a static empty list + "not yet implemented" message.
214
+
215
+ * Some diagnostics endpoints are present but not fully wired to UI.
216
+
217
+ ### 3.4 HTML tabs vs JS logic mismatch
218
+
219
+ `static/js/app.js` expects tab IDs like:
220
+
221
+ ```js
222
+ 'dashboard', 'market', 'models', 'sentiment', 'news', 'providers', 'diagnostics', 'api-explorer'
223
+ ```
224
+
225
+ But `templates/index.html` / `unified_dashboard.html` defines tabs like:
226
+
227
+ ```html
228
+ 'market', 'monitor', 'admin', 'hf', 'pools', 'logs', 'resources', 'reports', 'advanced'
229
+ ```
230
+
231
+ Thus:
232
+
233
+ * Tabs such as `'hf'`, `'pools'`, `'logs'`, `'resources'`, `'reports'`, `'advanced'` have **no corresponding JS case**.
234
+
235
+ * Tabs such as `'models'`, `'sentiment'`, `'diagnostics'`, `'api-explorer'` exist in JS but **not in HTML**.
236
+
237
+ Result: Many features (models UI, providers UI, diagnostics UI, API explorer) are effectively invisible or non-functional.
238
+
239
+ ### 3.5 Sentiment UI ID/function mismatch
240
+
241
+ HTML:
242
+
243
+ * Uses `id="sentimentInput"`, `id="sentimentResult"`, `id="sentimentDetails"`.
244
+
245
+ * Button calls `onclick="runSentiment()"`.
246
+
247
+ JS:
248
+
249
+ * Expects `id="sentiment-text"`, `id="sentiment-mode"`, `id="sentiment-model"`, `id="sentiment-result"`.
250
+
251
+ * Main handler is `analyzeSentiment()`.
252
+
253
+ Result: clicking the button throws `runSentiment is not defined`, and DOM IDs do not align.
254
+
255
+ ### 3.6 Missing containers for models/providers/diagnostics/API explorer
256
+
257
+ * `app.js` renders into elements like `#models-list`, `#models-status`, `#providers-list`, `#api-endpoint-list`, etc.
258
+
259
+ * These IDs are not present in `index.html`, or are named differently.
260
+
261
+ Result: even if backend endpoints work, UI has nowhere to render them.
262
+
263
+ ---
264
+
265
+ ## 4. Tasks – with explicit use of the new JSON files
266
+
267
+ ### 4.1 Backend – HF models & registry hardening (ai_models.py, config.py, api_server_extended.py)
268
+
269
+ * Keep the **existing structure and logic**, but:
270
+
271
+ 1. Implement robust auth and fallback:
272
+
273
+ * Respect `HF_MODE` (`off`, `public`, `auth`).
274
+
275
+ * In `"public"` mode:
276
+
277
+ * Do not rely on any expired or invalid token.
278
+
279
+ * Call `transformers.pipeline(...)` without `use_auth_token` or with a safe fallback.
280
+
281
+ * In `"auth"` mode:
282
+
283
+ * Use `HF_TOKEN` from env if present.
284
+
285
+ * If token is missing/invalid, log a concise warning, mark those models as failed, but do **not** crash startup.
286
+
287
+ 2. Per-task fallback chain:
288
+
289
+ * For each logical category:
290
+
291
+ * Crypto sentiment (`CRYPTO_SENTIMENT_MODELS`).
292
+
293
+ * Financial sentiment (`FINANCIAL_SENTIMENT_MODELS`).
294
+
295
+ * Social sentiment, news sentiment, summarization, etc.
296
+
297
+ * Define an ordered list of **public, reliable** model candidates.
298
+
299
+ * `initialize_models()` and `get_pipeline()` should try candidates in order and pick the first that loads.
300
+
301
+ * If all candidates in the chain fail:
302
+
303
+ * Register a clear `failed` entry in registry.
304
+
305
+ * Expose this failure cleanly via `/api/models/status` and `/api/models/list`.
306
+
307
+ 3. Registry & endpoints:
308
+
309
+ * `/api/models/status` should return:
310
+
311
+ * `status`: `"ok"`, `"partial"`, or `"disabled"`.
312
+
313
+ * `hf_mode`, `models_loaded`, `failed` with concise messages.
314
+
315
+ * `/api/models/list` should list **logical tasks** (e.g., `crypto_sent_0`, `financial_sent_0`) and indicate:
316
+
317
+ * Whether each is loaded.
318
+
319
+ * Underlying model ID.
320
+
321
+ * Any note (e.g., "requires HF auth", "fallback used").
322
+
323
+ 4. Make sure sentiment endpoints:
324
+
325
+ * `/api/sentiment/analyze`
326
+
327
+ * `/api/hf/run-sentiment`
328
+
329
+ * Any ensemble endpoints
330
+
331
+ correctly use the `ModelRegistry` and handle "no available model for this task" without crashing, returning a structured error to the UI.
332
+
333
+ > Do NOT delete existing task groups or endpoints – only strengthen and complete them.
334
+
335
+ ---
336
+
337
+ ### 4.2 Backend – integrate `providers_config_extended.json`
338
+
339
+ You **must** integrate this file as the primary providers registry.
340
+
341
+ 1. Locate `providers_config_extended.json` in the repo.
342
+
343
+ * If not already under `final/`, move or reference it there.
344
+
345
+ * Recommended path: `final/providers_config_extended.json`.
346
+
347
+ 2. In `api_server_extended.py`:
348
+
349
+ * Confirm or set:
350
+
351
+ ```python
352
+ PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
353
+ ```
354
+
355
+ * Fix `load_providers_config()` to:
356
+
357
+ * Load this JSON file.
358
+
359
+ * Validate that `config["providers"]` exists and is a dict.
360
+
361
+ * Return structured provider data.
362
+
363
+ 3. Update `/api/providers`:
364
+
365
+ * Use `providers_config_extended.json` to return a list of providers with fields like:
366
+
367
+ * `id` (key).
368
+
369
+ * `name`.
370
+
371
+ * `category`.
372
+
373
+ * `base_url`.
374
+
375
+ * `priority`.
376
+
377
+ * `weight`.
378
+
379
+ * `requires_auth`.
380
+
381
+ * `rate_limit`.
382
+
383
+ * Optionally include a computed `status` if you correlate with the auto-discovery report (see 4.3).
384
+
385
+ 4. Make sure `/api/providers` is **non-empty** and reflects the JSON content.
386
+
387
+ ---
388
+
389
+ ### 4.3 Backend – integrate `PROVIDER_AUTO_DISCOVERY_REPORT.json`
390
+
391
+ You must expose the auto-discovery report as part of the diagnostics / providers health.
392
+
393
+ 1. Choose a path, e.g.:
394
+
395
+ ```python
396
+ AUTO_DISCOVERY_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
397
+ ```
398
+
399
+ 2. Add or update endpoint(s) in `api_server_extended.py`, for example:
400
+
401
+ * `GET /api/providers/auto-discovery-report`:
402
+
403
+ * Returns the parsed JSON from `PROVIDER_AUTO_DISCOVERY_REPORT.json` (or a structured subset).
404
+
405
+ * Optionally `GET /api/providers/health-summary`:
406
+
407
+ * Returns a simplified summary:
408
+
409
+ * `total_active_providers`, `http_valid`, `http_invalid`, `hf_valid`, etc.
410
+
411
+ * Aggregated counts by `status` (`VALID`, `INVALID`, `CONDITIONALLY_AVAILABLE`).
412
+
413
+ 3. Optionally link provider configs to the discovery report:
414
+
415
+ * When constructing `/api/providers` output:
416
+
417
+ * If a provider ID from `providers_config_extended.json` appears in `http_providers.results[]`, merge:
418
+
419
+ * `status`, `requires_auth`, `error_reason`, `test_endpoint`, `response_time_ms`.
420
+
421
+ 4. Ensure these endpoints are resilient:
422
+
423
+ * If file is missing, return a clear error with `"ok": false`, `"error": "report file not found"`.
424
+
425
+ * If JSON parse fails, return an error message instead of raising.
426
+
427
+ ---
428
+
429
+ ### 4.4 Backend – integrate `all_apis_merged_2025.json` as a resources/API explorer source
430
+
431
+ You must wire this file to the **resources/API explorer** endpoints.
432
+
433
+ 1. Place or reference `all_apis_merged_2025.json` under `final/`, e.g.:
434
+
435
+ ```python
436
+ API_REGISTRY_PATH = WORKSPACE_ROOT / "all_apis_merged_2025.json"
437
+ ```
438
+
439
+ 2. In `api_server_extended.py`, create or update endpoints such as:
440
+
441
+ * `GET /api/resources/apis`:
442
+
443
+ * Returns:
444
+
445
+ * `metadata` from the JSON (`name`, `version`, `description`, `created_at`).
446
+
447
+ * High-level categories extracted from the text and structure (market data, explorers, RPC nodes, CORS proxies, etc.).
448
+
449
+ * `GET /api/resources/apis/raw`:
450
+
451
+ * Returns a trimmed version of `raw_files[]` (filename + first N characters) to avoid huge payloads.
452
+
453
+ * If you already have `/api/resources`, consider merging:
454
+
455
+ * Existing resources.
456
+
457
+ * The structured info from this registry into a single consolidated response.
458
+
459
+ 3. You do **not** need to fully parse all free-form text; focus on:
460
+
461
+ * Surfacing metadata.
462
+
463
+ * Providing an overview of categories.
464
+
465
+ * Exposing some example endpoints and usage hints to the UI.
466
+
467
+ ---
468
+
469
+ ### 4.5 Frontend – align tabs and use the new backend endpoints
470
+
471
+ In `templates/index.html` and `static/js/app.js`:
472
+
473
+ 1. **Synchronize tab IDs** between HTML and JS:
474
+
475
+ * For each visible tab button, make sure `switchTab('...')` uses a `tabId` that `loadTabData` recognizes.
476
+
477
+ Example mapping (you can refine but it must be consistent):
478
+
479
+ * `market` → `loadMarketData()`.
480
+
481
+ * `hf` → `loadModels()` + HF diagnostics (models status, HF mode).
482
+
483
+ * `resources` → call a new `loadResources()` that fetches `/api/resources/apis` and/or `/api/resources`.
484
+
485
+ * `logs` / `advanced` → call `loadDiagnostics()` and `loadAPIEndpoints()`.
486
+
487
+ 2. Either:
488
+
489
+ * Update `loadTabData` to handle `'hf'`, `'resources'`, `'logs'`, `'reports'`, `'advanced'`, `'monitor'`, `'admin'`.
490
+
491
+ **OR**
492
+
493
+ * Change HTML to use IDs that JS already expects (`models`, `providers`, `diagnostics`, `api-explorer`).
494
+
495
+ **But in any case, every tab shown in HTML must have a corresponding case in JS.**
496
+
497
+ 3. After this change:
498
+
499
+ * Clicking each tab must trigger at least one data-loading function and update UI.
500
+
501
+ ---
502
+
503
+ ### 4.6 Frontend – sentiment UI fix (and usage of models)
504
+
505
+ You must make the sentiment panel fully functional using the backend models.
506
+
507
+ 1. In HTML (`index.html`):
508
+
509
+ * Decide on final IDs and stick to them. For example:
510
+
511
+ ```html
512
+ <textarea id="sentiment-text"></textarea>
513
+ <select id="sentiment-mode">...</select>
514
+ <select id="sentiment-model">...</select>
515
+ <button onclick="analyzeSentiment()">Analyze Sentiment</button>
516
+ <div id="sentiment-result"></div>
517
+ <pre id="sentiment-details"></pre>
518
+ ```
519
+
520
+ * Or adapt JS to current IDs – just ensure both HTML and JS match.
521
+
522
+ 2. In `app.js`:
523
+
524
+ * Implement `analyzeSentiment()` (or `runSentiment()` that calls it) to:
525
+
526
+ * Read user input from the sentiment textarea and selectors.
527
+
528
+ * POST to the appropriate endpoint:
529
+
530
+ * `/api/sentiment/analyze` or `/api/hf/run-sentiment`.
531
+
532
+ * Render:
533
+
534
+ * A high-level label: positive/negative/neutral.
535
+
536
+ * Score/confidence.
537
+
538
+ * Any additional metadata.
539
+
540
+ * Use the **model registry** endpoints (e.g., `/api/models/list`) to populate the `sentiment-model` select with available sentiment models.
541
+
542
+ 3. Make sure:
543
+
544
+ * If no models are available for the chosen task, show a clear warning in the UI rather than crashing.
545
+
546
+ ---
547
+
548
+ ### 4.7 Frontend – providers & auto-discovery UI (using the new JSON-driven endpoints)
549
+
550
+ You must build UI that actually visualizes:
551
+
552
+ * Providers from `providers_config_extended.json` (via `/api/providers`).
553
+
554
+ * Auto-discovery status from `PROVIDER_AUTO_DISCOVERY_REPORT.json` (via new endpoints from section 4.3).
555
+
556
+ 1. In HTML (probably under **HF** or **Resources** or a dedicated **Providers** section):
557
+
558
+ * Add containers like:
559
+
560
+ ```html
561
+ <div id="providers-panel">
562
+ <div id="providers-summary"></div>
563
+ <table id="providers-list">...</table>
564
+ </div>
565
+
566
+ <div id="providers-health-panel">
567
+ <div id="providers-health-summary"></div>
568
+ <table id="providers-health-table">...</table>
569
+ </div>
570
+ ```
571
+
572
+ 2. In `app.js`:
573
+
574
+ * Implement or update `loadProviders()` to:
575
+
576
+ * Call `/api/providers` and render:
577
+
578
+ * Name, category, base_url, requires_auth, priority/weight.
579
+
580
+ * Call `/api/providers/auto-discovery-report` (or the endpoint you defined) and render:
581
+
582
+ * Total valid/invalid/conditional.
583
+
584
+ * A table listing provider_id, status, requires_auth, response_time_ms, error_reason, etc.
585
+
586
+ 3. Use simple but clear CSS from `main.css` to style the tables with:
587
+
588
+ * Status badges (`VALID` / `INVALID` / `CONDITIONALLY_AVAILABLE` with distinct styles).
589
+
590
+ * Highlight providers that are both present in config and validated successfully.
591
+
592
+ ---
593
+
594
+ ### 4.8 Frontend – API Explorer & Resources UI using `all_apis_merged_2025.json`
595
+
596
+ Under a suitable tab (e.g. **Resources**, **Advanced**, or a specific **API Explorer** tab):
597
+
598
+ 1. In HTML:
599
+
600
+ * Add containers like:
601
+
602
+ ```html
603
+ <section id="api-registry-section">
604
+ <div id="api-registry-metadata"></div>
605
+ <div id="api-registry-categories"></div>
606
+ <div id="api-registry-examples"></div>
607
+ </section>
608
+ ```
609
+
610
+ 2. In `app.js`:
611
+
612
+ * Implement `loadAPIRegistry()` or reuse `loadAPIEndpoints()` by extending it:
613
+
614
+ * Fetch `/api/resources/apis` (or `/api/resources` if merged) to get:
615
+
616
+ * `metadata` (name, version, description).
617
+
618
+ * High-level category descriptors.
619
+
620
+ * Render:
621
+
622
+ * Title & version.
623
+
624
+ * A list of main categories (market data, explorers, RPC, CORS proxy, etc).
625
+
626
+ * A small sample of example endpoints and usage patterns from `all_apis_merged_2025.json`.
627
+
628
+ 3. Optionally:
629
+
630
+ * Provide a small search/filter box to filter endpoints by keyword.
631
+
632
+ * Provide a "copy example URL" button.
633
+
634
+ ---
635
+
636
+ ### 4.9 CSS – keep style, polish where needed
637
+
638
+ In `static/css/main.css`:
639
+
640
+ * Without changing the design language, ensure:
641
+
642
+ * Newly-added panels (Providers, Auto-discovery, API Registry) have proper spacing, typography, and responsive behavior.
643
+
644
+ * Status labels for providers use distinct colors/icons.
645
+
646
+ * Loading and error states are visually clear (e.g., `.loading`, `.error-message` styles).
647
+
648
+ Do not introduce new libraries; just extend existing CSS.
649
+
650
+ ---
651
+
652
+ ## 5. Constraints & style rules
653
+
654
+ * Work **only** within:
655
+
656
+ * `final/api_server_extended.py`
657
+
658
+ * `final/ai_models.py`
659
+
660
+ * `final/config.py` (only small, safe updates)
661
+
662
+ * `final/templates/index.html` (and `unified_dashboard.html` if shared)
663
+
664
+ * `final/static/js/app.js`
665
+
666
+ * `final/static/css/main.css`
667
+
668
+ * Plus minor additions to wire in the three JSON files.
669
+
670
+ * Do **NOT**:
671
+
672
+ * Delete or move `hf_unified_server.py`, `api_server_extended.py`, or `app.py`.
673
+
674
+ * Introduce new frameworks (no React/Vue).
675
+
676
+ * Replace the HTML with a completely new page.
677
+
678
+ * Remove existing endpoints or DB tables.
679
+
680
+ * Do:
681
+
682
+ * Use the three JSON files as **authoritative data sources**:
683
+
684
+ * `providers_config_extended.json` for `/api/providers`.
685
+
686
+ * `PROVIDER_AUTO_DISCOVERY_REPORT.json` for providers diagnostics.
687
+
688
+ * `all_apis_merged_2025.json` for API explorer/resources.
689
+
690
+ * Keep HF model logic, but harden and complete it.
691
+
692
+ * Make all UI tabs functional and wired to their backend endpoints.
693
+
694
+ * Ensure the models that are pipelined are actually used in the sentiment and AI-related UI.
695
+
696
+ ---
697
+
698
+ ## 6. Acceptance criteria
699
+
700
+ 1. HF models & registry:
701
+
702
+ * Server boots without unhandled exceptions, even if some models are unavailable/private.
703
+
704
+ * At least one sentiment model is loaded in `"public"` mode if public models are accessible.
705
+
706
+ * `/api/models/status` and `/api/models/list` correctly reflect loaded and failed models.
707
+
708
+ 2. Providers & resources:
709
+
710
+ * `/api/providers` returns a list based on `providers_config_extended.json`.
711
+
712
+ * Auto-discovery endpoints expose data from `PROVIDER_AUTO_DISCOVERY_REPORT.json`.
713
+
714
+ * `/api/resources/apis` (or equivalent) uses `all_apis_merged_2025.json`.
715
+
716
+ 3. UI:
717
+
718
+ * All visible tabs call appropriate JS loaders and show content.
719
+
720
+ * Sentiment panel works end-to-end:
721
+
722
+ * User enters text → backend analyzes → UI shows label & score.
723
+
724
+ * HF/models tab shows model status and availability.
725
+
726
+ * Providers tab shows:
727
+
728
+ * Config providers list (name, category, base_url, auth requirement).
729
+
730
+ * Auto-discovery health summary & per-provider status.
731
+
732
+ * Resources/API explorer tab shows metadata + example APIs from `all_apis_merged_2025.json`.
733
+
734
+ 4. No regressions:
735
+
736
+ * Market/price charts still work.
737
+
738
+ * No new JS errors appear in normal user flows.
739
+
740
+ Once all of the above are satisfied, stop.
741
+
api_server_extended.py CHANGED
@@ -35,7 +35,8 @@ WORKSPACE_ROOT = Path("/app" if Path("/app").exists() else (Path("/workspace") i
35
  DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
36
  LOG_DIR = WORKSPACE_ROOT / "logs"
37
  PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
38
- APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
 
39
 
40
  # Ensure directories exist
41
  DB_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -156,26 +157,53 @@ def get_price_history_from_db(symbol: str, limit: int = 10) -> List[Dict[str, An
156
 
157
  # ===== Provider Management =====
158
  def load_providers_config() -> Dict[str, Any]:
159
- """Load providers from config file"""
160
  try:
161
  if PROVIDERS_CONFIG_PATH.exists():
162
- with open(PROVIDERS_CONFIG_PATH, 'r') as f:
163
- return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
164
  return {"providers": {}}
165
  except Exception as e:
166
- print(f"Error loading providers config: {e}")
167
  return {"providers": {}}
168
 
169
 
170
  def load_apl_report() -> Dict[str, Any]:
171
- """Load APL validation report"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
173
- if APL_REPORT_PATH.exists():
174
- with open(APL_REPORT_PATH, 'r') as f:
175
  return json.load(f)
176
  return {}
177
  except Exception as e:
178
- print(f"Error loading APL report: {e}")
179
  return {}
180
 
181
 
@@ -241,10 +269,16 @@ async def lifespan(app: FastAPI):
241
  _provider_state["providers"] = config.get("providers", {})
242
  print(f"✓ Loaded {len(_provider_state['providers'])} providers from config")
243
 
244
- # Load APL report
245
- apl_report = load_apl_report()
246
  if apl_report:
247
- print(f"✓ Loaded APL report with validation data")
 
 
 
 
 
 
248
 
249
  # Initialize AI models
250
  try:
@@ -374,7 +408,7 @@ async def get_status():
374
  "total_providers": len(providers),
375
  "validated_providers": validated_count,
376
  "database_status": "connected",
377
- "apl_available": APL_REPORT_PATH.exists(),
378
  "use_mock_data": USE_MOCK_DATA
379
  }
380
 
@@ -503,11 +537,14 @@ async def get_sentiment():
503
 
504
  @app.get("/api/resources")
505
  async def get_resources():
506
- """Get resources summary for HTML dashboard"""
507
  try:
 
 
 
 
508
  # Try to load resources from JSON files
509
  resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
510
- all_apis_json = WORKSPACE_ROOT / "all_apis_merged_2025.json"
511
 
512
  summary = {
513
  "total_resources": 0,
@@ -539,6 +576,7 @@ async def get_resources():
539
  return {
540
  "success": True,
541
  "summary": summary,
 
542
  "timestamp": datetime.now().isoformat()
543
  }
544
  except Exception as e:
@@ -554,6 +592,97 @@ async def get_resources():
554
  "timestamp": datetime.now().isoformat()
555
  }
556
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
  @app.get("/api/trending")
559
  async def get_trending():
@@ -588,22 +717,42 @@ async def get_trending():
588
  # ===== Providers Management Endpoints =====
589
  @app.get("/api/providers")
590
  async def get_providers():
591
- """Get all providers - REAL DATA from config + HF Models as providers"""
592
  config = load_providers_config()
593
  providers = config.get("providers", {})
594
 
 
 
 
 
 
 
 
595
  result = []
596
  for provider_id, provider_data in providers.items():
597
- result.append({
598
- "provider_id": provider_id,
 
 
 
 
599
  "name": provider_data.get("name", provider_id),
600
  "category": provider_data.get("category", "unknown"),
601
- "type": provider_data.get("type", "unknown"),
602
- "status": "validated" if provider_data.get("validated") else "unvalidated",
 
 
 
 
 
 
603
  "validated_at": provider_data.get("validated_at"),
604
- "response_time_ms": provider_data.get("response_time_ms"),
 
 
605
  "added_by": provider_data.get("added_by", "manual")
606
- })
 
607
 
608
  # Add HF Models as providers
609
  try:
@@ -611,6 +760,7 @@ async def get_providers():
611
  for model_key, spec in MODEL_SPECS.items():
612
  is_loaded = model_key in _registry._pipelines
613
  result.append({
 
614
  "provider_id": f"hf_model_{model_key}",
615
  "name": f"HF Model: {spec.model_id}",
616
  "category": spec.category,
@@ -624,12 +774,12 @@ async def get_providers():
624
  "added_by": "hf_models"
625
  })
626
  except Exception as e:
627
- print(f"Could not add HF models as providers: {e}")
628
 
629
  return {
630
  "providers": result,
631
  "total": len(result),
632
- "source": "providers_config_extended.json + HF Models (Real Data)"
633
  }
634
 
635
 
@@ -750,9 +900,9 @@ async def run_diagnostics(auto_fix: bool = False):
750
  if not PROVIDERS_CONFIG_PATH.exists():
751
  issues.append({"type": "config", "message": "Providers config not found"})
752
 
753
- # Check APL report
754
- if not APL_REPORT_PATH.exists():
755
- issues.append({"type": "apl", "message": "APL report not found"})
756
 
757
  return {
758
  "status": "completed",
@@ -810,43 +960,72 @@ async def run_apl_scan():
810
 
811
  @app.get("/api/apl/report")
812
  async def get_apl_report():
813
- """Get APL validation report"""
814
- report = load_apl_report()
 
 
 
 
 
815
 
816
  if not report:
817
  return {
818
- "status": "not_available",
819
- "message": "APL report not found. Run APL scan first."
 
820
  }
821
 
822
- return report
823
-
 
 
 
824
 
825
- @app.get("/api/apl/summary")
826
- async def get_apl_summary():
827
- """Get APL summary statistics"""
828
- report = load_apl_report()
829
 
830
  if not report or "stats" not in report:
831
  return {
832
- "status": "not_available",
833
- "message": "APL report not found"
 
834
  }
835
 
836
  stats = report.get("stats", {})
 
 
 
 
 
 
 
 
 
 
837
  return {
838
- "http_candidates": stats.get("total_http_candidates", 0),
839
- "http_valid": stats.get("http_valid", 0),
840
- "http_invalid": stats.get("http_invalid", 0),
841
- "http_conditional": stats.get("http_conditional", 0),
842
- "hf_candidates": stats.get("total_hf_candidates", 0),
843
- "hf_valid": stats.get("hf_valid", 0),
844
- "hf_invalid": stats.get("hf_invalid", 0),
845
- "hf_conditional": stats.get("hf_conditional", 0),
846
- "total_active": stats.get("total_active_providers", 0),
847
- "timestamp": stats.get("timestamp", "")
 
 
 
 
848
  }
849
 
 
 
 
 
 
850
 
851
  # ===== HF Models Endpoints =====
852
  @app.get("/api/hf/models")
 
35
  DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
36
  LOG_DIR = WORKSPACE_ROOT / "logs"
37
  PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
38
+ AUTO_DISCOVERY_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
39
+ API_REGISTRY_PATH = WORKSPACE_ROOT / "all_apis_merged_2025.json"
40
 
41
  # Ensure directories exist
42
  DB_PATH.parent.mkdir(parents=True, exist_ok=True)
 
157
 
158
  # ===== Provider Management =====
159
  def load_providers_config() -> Dict[str, Any]:
160
+ """Load providers from providers_config_extended.json"""
161
  try:
162
  if PROVIDERS_CONFIG_PATH.exists():
163
+ with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
164
+ config = json.load(f)
165
+ # Validate structure
166
+ if not isinstance(config, dict):
167
+ logger.warning("Providers config is not a dict, returning empty")
168
+ return {"providers": {}}
169
+ if "providers" not in config:
170
+ logger.warning("Providers config missing 'providers' key, adding it")
171
+ config["providers"] = {}
172
+ return config
173
+ logger.warning(f"Providers config file not found at {PROVIDERS_CONFIG_PATH}")
174
+ return {"providers": {}}
175
+ except json.JSONDecodeError as e:
176
+ logger.error(f"JSON decode error loading providers config: {e}")
177
  return {"providers": {}}
178
  except Exception as e:
179
+ logger.error(f"Error loading providers config: {e}")
180
  return {"providers": {}}
181
 
182
 
183
  def load_apl_report() -> Dict[str, Any]:
184
+ """Load APL validation report (alias for auto-discovery report)"""
185
+ return load_auto_discovery_report()
186
+
187
+ def load_auto_discovery_report() -> Dict[str, Any]:
188
+ """Load PROVIDER_AUTO_DISCOVERY_REPORT.json"""
189
+ try:
190
+ if AUTO_DISCOVERY_REPORT_PATH.exists():
191
+ with open(AUTO_DISCOVERY_REPORT_PATH, 'r', encoding='utf-8') as f:
192
+ return json.load(f)
193
+ return {}
194
+ except Exception as e:
195
+ logger.error(f"Error loading auto-discovery report: {e}")
196
+ return {}
197
+
198
+ def load_api_registry() -> Dict[str, Any]:
199
+ """Load all_apis_merged_2025.json"""
200
  try:
201
+ if API_REGISTRY_PATH.exists():
202
+ with open(API_REGISTRY_PATH, 'r', encoding='utf-8') as f:
203
  return json.load(f)
204
  return {}
205
  except Exception as e:
206
+ logger.error(f"Error loading API registry: {e}")
207
  return {}
208
 
209
 
 
269
  _provider_state["providers"] = config.get("providers", {})
270
  print(f"✓ Loaded {len(_provider_state['providers'])} providers from config")
271
 
272
+ # Load auto-discovery report
273
+ apl_report = load_auto_discovery_report()
274
  if apl_report:
275
+ print(f"✓ Loaded auto-discovery report with validation data")
276
+
277
+ # Load API registry
278
+ api_registry = load_api_registry()
279
+ if api_registry:
280
+ metadata = api_registry.get("metadata", {})
281
+ print(f"✓ Loaded API registry: {metadata.get('name', 'unknown')} v{metadata.get('version', 'unknown')}")
282
 
283
  # Initialize AI models
284
  try:
 
408
  "total_providers": len(providers),
409
  "validated_providers": validated_count,
410
  "database_status": "connected",
411
+ "apl_available": AUTO_DISCOVERY_REPORT_PATH.exists(),
412
  "use_mock_data": USE_MOCK_DATA
413
  }
414
 
 
537
 
538
  @app.get("/api/resources")
539
  async def get_resources():
540
+ """Get resources summary for HTML dashboard (includes API registry metadata)"""
541
  try:
542
+ # Load API registry for metadata
543
+ api_registry = load_api_registry()
544
+ metadata = api_registry.get("metadata", {}) if api_registry else {}
545
+
546
  # Try to load resources from JSON files
547
  resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
 
548
 
549
  summary = {
550
  "total_resources": 0,
 
576
  return {
577
  "success": True,
578
  "summary": summary,
579
+ "api_registry_metadata": metadata,
580
  "timestamp": datetime.now().isoformat()
581
  }
582
  except Exception as e:
 
592
  "timestamp": datetime.now().isoformat()
593
  }
594
 
595
+ @app.get("/api/resources/apis")
596
+ async def get_resources_apis():
597
+ """Get API registry from all_apis_merged_2025.json"""
598
+ registry = load_api_registry()
599
+
600
+ if not registry:
601
+ return {
602
+ "ok": False,
603
+ "error": "API registry file not found",
604
+ "message": f"Registry file not found at {API_REGISTRY_PATH}"
605
+ }
606
+
607
+ metadata = registry.get("metadata", {})
608
+ raw_files = registry.get("raw_files", [])
609
+
610
+ # Extract categories from raw file content (basic parsing)
611
+ categories = set()
612
+ for raw_file in raw_files[:5]: # Limit to first 5 files for performance
613
+ content = raw_file.get("content", "")
614
+ # Simple category detection from content
615
+ if "market data" in content.lower() or "price" in content.lower():
616
+ categories.add("market_data")
617
+ if "explorer" in content.lower() or "blockchain" in content.lower():
618
+ categories.add("block_explorer")
619
+ if "rpc" in content.lower() or "node" in content.lower():
620
+ categories.add("rpc_nodes")
621
+ if "cors" in content.lower() or "proxy" in content.lower():
622
+ categories.add("cors_proxy")
623
+ if "news" in content.lower():
624
+ categories.add("news")
625
+ if "sentiment" in content.lower() or "fear" in content.lower():
626
+ categories.add("sentiment")
627
+ if "whale" in content.lower():
628
+ categories.add("whale_tracking")
629
+
630
+ # Provide trimmed raw files (first 500 chars each)
631
+ trimmed_files = []
632
+ for raw_file in raw_files[:10]: # Limit to 10 files
633
+ content = raw_file.get("content", "")
634
+ trimmed_files.append({
635
+ "filename": raw_file.get("filename", ""),
636
+ "preview": content[:500] + "..." if len(content) > 500 else content,
637
+ "size": len(content)
638
+ })
639
+
640
+ return {
641
+ "ok": True,
642
+ "metadata": {
643
+ "name": metadata.get("name", ""),
644
+ "version": metadata.get("version", ""),
645
+ "description": metadata.get("description", ""),
646
+ "created_at": metadata.get("created_at", ""),
647
+ "source_files": metadata.get("source_files", [])
648
+ },
649
+ "categories": list(categories),
650
+ "raw_files_preview": trimmed_files,
651
+ "total_raw_files": len(raw_files),
652
+ "source": "all_apis_merged_2025.json"
653
+ }
654
+
655
+ @app.get("/api/resources/apis/raw")
656
+ async def get_resources_apis_raw():
657
+ """Get raw files from API registry (trimmed to avoid huge payloads)"""
658
+ registry = load_api_registry()
659
+
660
+ if not registry:
661
+ return {
662
+ "ok": False,
663
+ "error": "API registry file not found"
664
+ }
665
+
666
+ raw_files = registry.get("raw_files", [])
667
+
668
+ # Return trimmed versions (first 1000 chars each, max 20 files)
669
+ trimmed = []
670
+ for raw_file in raw_files[:20]:
671
+ content = raw_file.get("content", "")
672
+ trimmed.append({
673
+ "filename": raw_file.get("filename", ""),
674
+ "preview": content[:1000] + "..." if len(content) > 1000 else content,
675
+ "full_size": len(content)
676
+ })
677
+
678
+ return {
679
+ "ok": True,
680
+ "files": trimmed,
681
+ "total_files": len(raw_files),
682
+ "showing": min(20, len(raw_files)),
683
+ "source": "all_apis_merged_2025.json"
684
+ }
685
+
686
 
687
  @app.get("/api/trending")
688
  async def get_trending():
 
717
  # ===== Providers Management Endpoints =====
718
  @app.get("/api/providers")
719
  async def get_providers():
720
+ """Get all providers from providers_config_extended.json + HF Models + auto-discovery status"""
721
  config = load_providers_config()
722
  providers = config.get("providers", {})
723
 
724
+ # Load auto-discovery report to merge status
725
+ discovery_report = load_auto_discovery_report()
726
+ discovery_results = {}
727
+ if discovery_report and "http_providers" in discovery_report:
728
+ for result in discovery_report["http_providers"].get("results", []):
729
+ discovery_results[result.get("provider_id")] = result
730
+
731
  result = []
732
  for provider_id, provider_data in providers.items():
733
+ # Merge with auto-discovery data if available
734
+ discovery_data = discovery_results.get(provider_id, {})
735
+
736
+ provider_entry = {
737
+ "id": provider_id,
738
+ "provider_id": provider_id, # Keep for backward compatibility
739
  "name": provider_data.get("name", provider_id),
740
  "category": provider_data.get("category", "unknown"),
741
+ "base_url": provider_data.get("base_url", ""),
742
+ "type": provider_data.get("type", "http"),
743
+ "priority": provider_data.get("priority", 0),
744
+ "weight": provider_data.get("weight", 0),
745
+ "requires_auth": provider_data.get("requires_auth", False),
746
+ "rate_limit": provider_data.get("rate_limit", {}),
747
+ "endpoints": provider_data.get("endpoints", {}),
748
+ "status": discovery_data.get("status", "UNKNOWN") if discovery_data else "unvalidated",
749
  "validated_at": provider_data.get("validated_at"),
750
+ "response_time_ms": discovery_data.get("response_time_ms") or provider_data.get("response_time_ms"),
751
+ "error_reason": discovery_data.get("error_reason"),
752
+ "test_endpoint": discovery_data.get("test_endpoint"),
753
  "added_by": provider_data.get("added_by", "manual")
754
+ }
755
+ result.append(provider_entry)
756
 
757
  # Add HF Models as providers
758
  try:
 
760
  for model_key, spec in MODEL_SPECS.items():
761
  is_loaded = model_key in _registry._pipelines
762
  result.append({
763
+ "id": f"hf_model_{model_key}",
764
  "provider_id": f"hf_model_{model_key}",
765
  "name": f"HF Model: {spec.model_id}",
766
  "category": spec.category,
 
774
  "added_by": "hf_models"
775
  })
776
  except Exception as e:
777
+ logger.warning(f"Could not add HF models as providers: {e}")
778
 
779
  return {
780
  "providers": result,
781
  "total": len(result),
782
+ "source": "providers_config_extended.json + HF Models + Auto-Discovery Report"
783
  }
784
 
785
 
 
900
  if not PROVIDERS_CONFIG_PATH.exists():
901
  issues.append({"type": "config", "message": "Providers config not found"})
902
 
903
+ # Check auto-discovery report
904
+ if not AUTO_DISCOVERY_REPORT_PATH.exists():
905
+ issues.append({"type": "auto_discovery", "message": "Auto-discovery report not found"})
906
 
907
  return {
908
  "status": "completed",
 
960
 
961
  @app.get("/api/apl/report")
962
  async def get_apl_report():
963
+ """Get APL validation report (alias for auto-discovery report)"""
964
+ return await get_providers_auto_discovery_report()
965
+
966
+ @app.get("/api/providers/auto-discovery-report")
967
+ async def get_providers_auto_discovery_report():
968
+ """Get PROVIDER_AUTO_DISCOVERY_REPORT.json"""
969
+ report = load_auto_discovery_report()
970
 
971
  if not report:
972
  return {
973
+ "ok": False,
974
+ "error": "Auto-discovery report file not found",
975
+ "message": f"Report file not found at {AUTO_DISCOVERY_REPORT_PATH}"
976
  }
977
 
978
+ return {
979
+ "ok": True,
980
+ "report": report,
981
+ "source": "PROVIDER_AUTO_DISCOVERY_REPORT.json"
982
+ }
983
 
984
+ @app.get("/api/providers/health-summary")
985
+ async def get_providers_health_summary():
986
+ """Get simplified health summary from auto-discovery report"""
987
+ report = load_auto_discovery_report()
988
 
989
  if not report or "stats" not in report:
990
  return {
991
+ "ok": False,
992
+ "error": "Auto-discovery report not found or invalid",
993
+ "message": f"Report file not found at {AUTO_DISCOVERY_REPORT_PATH}"
994
  }
995
 
996
  stats = report.get("stats", {})
997
+ http_providers = report.get("http_providers", {})
998
+ hf_providers = report.get("hf_providers", {})
999
+
1000
+ # Count by status
1001
+ status_counts = {"VALID": 0, "INVALID": 0, "CONDITIONALLY_AVAILABLE": 0}
1002
+ for result in http_providers.get("results", []):
1003
+ status = result.get("status", "UNKNOWN")
1004
+ if status in status_counts:
1005
+ status_counts[status] += 1
1006
+
1007
  return {
1008
+ "ok": True,
1009
+ "summary": {
1010
+ "total_active_providers": stats.get("total_active_providers", 0),
1011
+ "http_valid": stats.get("http_valid", 0),
1012
+ "http_invalid": stats.get("http_invalid", 0),
1013
+ "http_conditional": stats.get("http_conditional", 0),
1014
+ "hf_valid": stats.get("hf_valid", 0),
1015
+ "hf_invalid": stats.get("hf_invalid", 0),
1016
+ "hf_conditional": stats.get("hf_conditional", 0),
1017
+ "status_breakdown": status_counts,
1018
+ "execution_time_sec": stats.get("execution_time_sec", 0),
1019
+ "timestamp": stats.get("timestamp", "")
1020
+ },
1021
+ "source": "PROVIDER_AUTO_DISCOVERY_REPORT.json"
1022
  }
1023
 
1024
+ @app.get("/api/apl/summary")
1025
+ async def get_apl_summary():
1026
+ """Get APL summary statistics (alias for health-summary)"""
1027
+ return await get_providers_health_summary()
1028
+
1029
 
1030
  # ===== HF Models Endpoints =====
1031
  @app.get("/api/hf/models")
static/js/app.js CHANGED
@@ -46,15 +46,40 @@ function initTabs() {
46
  });
47
  }
48
 
49
- // Load tab-specific data
50
  function loadTabData(tabId) {
51
  switch(tabId) {
52
  case 'dashboard':
53
- loadDashboard();
54
- break;
55
  case 'market':
56
  loadMarketData();
57
  break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  case 'models':
59
  loadModels();
60
  break;
@@ -72,7 +97,6 @@ function loadTabData(tabId) {
72
  loadDiagnostics();
73
  break;
74
  case 'api-explorer':
75
- // Load available endpoints
76
  loadAPIEndpoints();
77
  break;
78
  }
@@ -925,72 +949,123 @@ async function loadNews() {
925
  // Load Providers
926
  async function loadProviders() {
927
  try {
928
- const response = await fetch('/api/providers');
929
- const data = await response.json();
 
 
 
930
 
931
- const providers = data.providers || data || [];
 
932
 
933
- if (providers.length > 0) {
934
- const providersDiv = document.getElementById('providers-list');
935
- providersDiv.innerHTML = `
936
- <div style="overflow-x: auto;">
937
- <table>
938
- <thead>
939
- <tr>
940
- <th>ID</th>
941
- <th>نام</th>
942
- <th>دسته</th>
943
- <th>نوع</th>
944
- <th>وضعیت</th>
945
- <th>جزئیات</th>
946
- </tr>
947
- </thead>
948
- <tbody>
949
- ${providers.map(provider => {
950
- const status = provider.status || 'unknown';
951
- const statusConfig = {
952
- 'validated': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
953
- 'available': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ در دسترس' },
954
- 'online': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ آنلاین' },
955
- 'unvalidated': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ نامعتبر' },
956
- 'not_loaded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ بارگذاری نشده' },
957
- 'offline': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: ' آفلاین' },
958
- 'degraded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ تخریب شده' }
959
- };
960
- const statusInfo = statusConfig[status] || { color: 'var(--text-secondary)', bg: 'rgba(156, 163, 175, 0.2)', text: ' نامشخص' };
961
-
962
- return `
963
- <tr>
964
- <td>${provider.provider_id || provider.id || '-'}</td>
965
- <td><strong>${provider.name || 'Unknown'}</strong></td>
966
- <td>${provider.category || '-'}</td>
967
- <td>${provider.type || '-'}</td>
968
- <td>
969
- <span style="padding: 5px 10px; border-radius: 5px; background: ${statusInfo.bg}; color: ${statusInfo.color}; font-size: 12px;">
970
- ${statusInfo.text}
971
- </span>
972
- </td>
973
- <td>
974
- ${provider.response_time_ms ? `<span style="font-size: 12px; color: var(--text-secondary);">${provider.response_time_ms}ms</span>` : ''}
975
- ${provider.endpoint ? `<a href="${provider.endpoint}" target="_blank" style="color: var(--primary); font-size: 12px;">🔗</a>` : ''}
976
- </td>
977
- </tr>
978
- `;
979
- }).join('')}
980
- </tbody>
981
- </table>
982
- </div>
983
- <div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
984
- <strong>کل Providerها:</strong> ${data.total || providers.length}
985
- </div>
986
- `;
987
- } else {
988
- document.getElementById('providers-list').innerHTML = '<div class="alert alert-warning">هیچ Providerی یافت نشد</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  }
 
990
  } catch (error) {
991
  console.error('Error loading providers:', error);
992
  showError('خطا در بارگذاری Providerها');
993
- document.getElementById('providers-list').innerHTML = '<div class="alert alert-error">خطا در بارگذاری Providerها</div>';
 
 
 
994
  }
995
  }
996
 
@@ -1279,3 +1354,199 @@ function showSuccess(message) {
1279
  setTimeout(() => alert.remove(), 5000);
1280
  }
1281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  });
47
  }
48
 
49
+ // Load tab-specific data - synchronized with HTML tabs
50
  function loadTabData(tabId) {
51
  switch(tabId) {
52
  case 'dashboard':
 
 
53
  case 'market':
54
  loadMarketData();
55
  break;
56
+ case 'monitor':
57
+ loadMonitorData();
58
+ break;
59
+ case 'advanced':
60
+ loadAdvancedData();
61
+ break;
62
+ case 'admin':
63
+ loadAdminData();
64
+ break;
65
+ case 'hf':
66
+ loadHFHealth();
67
+ loadModels();
68
+ break;
69
+ case 'pools':
70
+ loadPools();
71
+ break;
72
+ case 'logs':
73
+ loadLogs();
74
+ break;
75
+ case 'resources':
76
+ loadResources();
77
+ loadAPIRegistry();
78
+ break;
79
+ case 'reports':
80
+ loadReports();
81
+ break;
82
+ // Legacy tab names for backward compatibility
83
  case 'models':
84
  loadModels();
85
  break;
 
97
  loadDiagnostics();
98
  break;
99
  case 'api-explorer':
 
100
  loadAPIEndpoints();
101
  break;
102
  }
 
949
  // Load Providers
950
  async function loadProviders() {
951
  try {
952
+ // Load providers and auto-discovery health summary in parallel
953
+ const [providersRes, healthRes] = await Promise.all([
954
+ fetch('/api/providers'),
955
+ fetch('/api/providers/health-summary').catch(() => null) // Optional
956
+ ]);
957
 
958
+ const providersData = await providersRes.json();
959
+ const providers = providersData.providers || providersData || [];
960
 
961
+ // Update providers list
962
+ const providersDiv = document.getElementById('providers-list');
963
+ if (providersDiv) {
964
+ if (providers.length > 0) {
965
+ providersDiv.innerHTML = `
966
+ <div style="overflow-x: auto;">
967
+ <table>
968
+ <thead>
969
+ <tr>
970
+ <th>ID</th>
971
+ <th>نام</th>
972
+ <th>دسته</th>
973
+ <th>نوع</th>
974
+ <th>وضعیت</th>
975
+ <th>جزئیات</th>
976
+ </tr>
977
+ </thead>
978
+ <tbody>
979
+ ${providers.map(provider => {
980
+ const status = provider.status || 'unknown';
981
+ const statusConfig = {
982
+ 'VALID': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
983
+ 'validated': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: ' معتبر' },
984
+ 'available': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: ' در دسترس' },
985
+ 'online': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: ' آنلاین' },
986
+ 'CONDITIONALLY_AVAILABLE': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ شرطی' },
987
+ 'INVALID': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ نامعتبر' },
988
+ 'unvalidated': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ نامعتبر' },
989
+ 'not_loaded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ بارگذاری نشده' },
990
+ 'offline': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ آفلاین' },
991
+ 'degraded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ تخریب شده' }
992
+ };
993
+ const statusInfo = statusConfig[status] || { color: 'var(--text-secondary)', bg: 'rgba(156, 163, 175, 0.2)', text: '❓ نامشخص' };
994
+
995
+ return `
996
+ <tr>
997
+ <td>${provider.provider_id || provider.id || '-'}</td>
998
+ <td><strong>${provider.name || 'Unknown'}</strong></td>
999
+ <td>${provider.category || '-'}</td>
1000
+ <td>${provider.type || '-'}</td>
1001
+ <td>
1002
+ <span style="padding: 5px 10px; border-radius: 5px; background: ${statusInfo.bg}; color: ${statusInfo.color}; font-size: 12px;">
1003
+ ${statusInfo.text}
1004
+ </span>
1005
+ </td>
1006
+ <td>
1007
+ ${provider.response_time_ms ? `<span style="font-size: 12px; color: var(--text-secondary);">${provider.response_time_ms}ms</span>` : ''}
1008
+ ${provider.endpoint ? `<a href="${provider.endpoint}" target="_blank" style="color: var(--primary); font-size: 12px;">🔗</a>` : ''}
1009
+ ${provider.error_reason ? `<span style="font-size: 11px; color: var(--danger);" title="${provider.error_reason}">⚠️</span>` : ''}
1010
+ </td>
1011
+ </tr>
1012
+ `;
1013
+ }).join('')}
1014
+ </tbody>
1015
+ </table>
1016
+ </div>
1017
+ <div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
1018
+ <strong>کل Providerها:</strong> ${providersData.total || providers.length}
1019
+ </div>
1020
+ `;
1021
+ } else {
1022
+ providersDiv.innerHTML = '<div class="alert alert-warning">هیچ Providerی یافت نشد</div>';
1023
+ }
1024
+ }
1025
+
1026
+ // Update health summary if available
1027
+ if (healthRes) {
1028
+ try {
1029
+ const healthData = await healthRes.json();
1030
+ const healthSummaryDiv = document.getElementById('providers-health-summary');
1031
+ if (healthSummaryDiv && healthData.ok && healthData.summary) {
1032
+ const summary = healthData.summary;
1033
+ healthSummaryDiv.innerHTML = `
1034
+ <div class="card">
1035
+ <h3>Provider Health Summary</h3>
1036
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">
1037
+ <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border-left: 4px solid var(--success);">
1038
+ <div style="font-size: 24px; font-weight: bold; color: var(--success);">${summary.total_active_providers || 0}</div>
1039
+ <div style="font-size: 12px; color: var(--text-secondary);">Total Active</div>
1040
+ </div>
1041
+ <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border-left: 4px solid var(--success);">
1042
+ <div style="font-size: 24px; font-weight: bold; color: var(--success);">${summary.http_valid || 0}</div>
1043
+ <div style="font-size: 12px; color: var(--text-secondary);">HTTP Valid</div>
1044
+ </div>
1045
+ <div style="padding: 15px; background: rgba(239, 68, 68, 0.1); border-radius: 10px; border-left: 4px solid var(--danger);">
1046
+ <div style="font-size: 24px; font-weight: bold; color: var(--danger);">${summary.http_invalid || 0}</div>
1047
+ <div style="font-size: 12px; color: var(--text-secondary);">HTTP Invalid</div>
1048
+ </div>
1049
+ <div style="padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 4px solid var(--warning);">
1050
+ <div style="font-size: 24px; font-weight: bold; color: var(--warning);">${summary.http_conditional || 0}</div>
1051
+ <div style="font-size: 12px; color: var(--text-secondary);">Conditional</div>
1052
+ </div>
1053
+ </div>
1054
+ </div>
1055
+ `;
1056
+ }
1057
+ } catch (e) {
1058
+ console.warn('Could not load health summary:', e);
1059
+ }
1060
  }
1061
+
1062
  } catch (error) {
1063
  console.error('Error loading providers:', error);
1064
  showError('خطا در بارگذاری Providerها');
1065
+ const providersDiv = document.getElementById('providers-list');
1066
+ if (providersDiv) {
1067
+ providersDiv.innerHTML = '<div class="alert alert-error">خطا در بارگذاری Providerها</div>';
1068
+ }
1069
  }
1070
  }
1071
 
 
1354
  setTimeout(() => alert.remove(), 5000);
1355
  }
1356
 
1357
+ // Additional tab loaders for HTML tabs
1358
+ async function loadMonitorData() {
1359
+ // Load API monitor data
1360
+ try {
1361
+ const response = await fetch('/api/status');
1362
+ const data = await response.json();
1363
+ const monitorContainer = document.getElementById('monitor-content');
1364
+ if (monitorContainer) {
1365
+ monitorContainer.innerHTML = `
1366
+ <div class="card">
1367
+ <h3>API Status</h3>
1368
+ <pre>${JSON.stringify(data, null, 2)}</pre>
1369
+ </div>
1370
+ `;
1371
+ }
1372
+ } catch (error) {
1373
+ console.error('Error loading monitor data:', error);
1374
+ }
1375
+ }
1376
+
1377
+ async function loadAdvancedData() {
1378
+ // Load advanced/API explorer data
1379
+ loadAPIEndpoints();
1380
+ loadDiagnostics();
1381
+ }
1382
+
1383
+ async function loadAdminData() {
1384
+ // Load admin panel data
1385
+ try {
1386
+ const [providersRes, modelsRes] = await Promise.all([
1387
+ fetch('/api/providers'),
1388
+ fetch('/api/models/status')
1389
+ ]);
1390
+ const providers = await providersRes.json();
1391
+ const models = await modelsRes.json();
1392
+
1393
+ const adminContainer = document.getElementById('admin-content');
1394
+ if (adminContainer) {
1395
+ adminContainer.innerHTML = `
1396
+ <div class="card">
1397
+ <h3>System Status</h3>
1398
+ <p>Providers: ${providers.total || 0}</p>
1399
+ <p>Models: ${models.models_loaded || 0} loaded</p>
1400
+ </div>
1401
+ `;
1402
+ }
1403
+ } catch (error) {
1404
+ console.error('Error loading admin data:', error);
1405
+ }
1406
+ }
1407
+
1408
+ async function loadHFHealth() {
1409
+ // Load HF models health status
1410
+ try {
1411
+ const response = await fetch('/api/models/status');
1412
+ const data = await response.json();
1413
+ const hfContainer = document.getElementById('hf-status');
1414
+ if (hfContainer) {
1415
+ hfContainer.innerHTML = `
1416
+ <div class="card">
1417
+ <h3>HF Models Status</h3>
1418
+ <p>Mode: ${data.hf_mode || 'unknown'}</p>
1419
+ <p>Loaded: ${data.models_loaded || 0}</p>
1420
+ <p>Failed: ${data.failed_count || 0}</p>
1421
+ <p>Status: ${data.status || 'unknown'}</p>
1422
+ </div>
1423
+ `;
1424
+ }
1425
+ } catch (error) {
1426
+ console.error('Error loading HF health:', error);
1427
+ }
1428
+ }
1429
+
1430
+ async function loadPools() {
1431
+ // Load provider pools
1432
+ try {
1433
+ const response = await fetch('/api/pools');
1434
+ const data = await response.json();
1435
+ const poolsContainer = document.getElementById('pools-content');
1436
+ if (poolsContainer) {
1437
+ poolsContainer.innerHTML = `
1438
+ <div class="card">
1439
+ <h3>Provider Pools</h3>
1440
+ <p>${data.message || 'No pools available'}</p>
1441
+ <pre>${JSON.stringify(data, null, 2)}</pre>
1442
+ </div>
1443
+ `;
1444
+ }
1445
+ } catch (error) {
1446
+ console.error('Error loading pools:', error);
1447
+ }
1448
+ }
1449
+
1450
+ async function loadLogs() {
1451
+ // Load recent logs
1452
+ try {
1453
+ const response = await fetch('/api/logs/recent');
1454
+ const data = await response.json();
1455
+ const logsContainer = document.getElementById('logs-content');
1456
+ if (logsContainer) {
1457
+ const logsHtml = data.logs && data.logs.length > 0
1458
+ ? data.logs.map(log => `<div class="log-entry">${JSON.stringify(log)}</div>`).join('')
1459
+ : '<p>No logs available</p>';
1460
+ logsContainer.innerHTML = `<div class="card"><h3>Recent Logs</h3>${logsHtml}</div>`;
1461
+ }
1462
+ } catch (error) {
1463
+ console.error('Error loading logs:', error);
1464
+ }
1465
+ }
1466
+
1467
+ async function loadReports() {
1468
+ // Load reports/analytics
1469
+ try {
1470
+ const response = await fetch('/api/providers/health-summary');
1471
+ const data = await response.json();
1472
+ const reportsContainer = document.getElementById('reports-content');
1473
+ if (reportsContainer) {
1474
+ reportsContainer.innerHTML = `
1475
+ <div class="card">
1476
+ <h3>Provider Health Report</h3>
1477
+ <pre>${JSON.stringify(data, null, 2)}</pre>
1478
+ </div>
1479
+ `;
1480
+ }
1481
+ } catch (error) {
1482
+ console.error('Error loading reports:', error);
1483
+ }
1484
+ }
1485
+
1486
+ async function loadResources() {
1487
+ // Load resources summary
1488
+ try {
1489
+ const response = await fetch('/api/resources');
1490
+ const data = await response.json();
1491
+ const resourcesContainer = document.getElementById('resources-summary');
1492
+ if (resourcesContainer) {
1493
+ const summary = data.summary || {};
1494
+ resourcesContainer.innerHTML = `
1495
+ <div class="card">
1496
+ <h3>Resources Summary</h3>
1497
+ <p>Total: ${summary.total_resources || 0}</p>
1498
+ <p>Free: ${summary.free_resources || 0}</p>
1499
+ <p>Models: ${summary.models_available || 0}</p>
1500
+ </div>
1501
+ `;
1502
+ }
1503
+ } catch (error) {
1504
+ console.error('Error loading resources:', error);
1505
+ }
1506
+ }
1507
+
1508
+ async function loadAPIRegistry() {
1509
+ // Load API registry from all_apis_merged_2025.json
1510
+ try {
1511
+ const response = await fetch('/api/resources/apis');
1512
+ const data = await response.json();
1513
+
1514
+ if (!data.ok) {
1515
+ console.warn('API registry not available:', data.error);
1516
+ return;
1517
+ }
1518
+
1519
+ const registryContainer = document.getElementById('api-registry-section');
1520
+ if (registryContainer) {
1521
+ const metadata = data.metadata || {};
1522
+ const categories = data.categories || [];
1523
+
1524
+ registryContainer.innerHTML = `
1525
+ <div class="card">
1526
+ <h3>API Registry: ${metadata.name || 'Unknown'}</h3>
1527
+ <p>Version: ${metadata.version || 'N/A'}</p>
1528
+ <p>Description: ${metadata.description || 'N/A'}</p>
1529
+ <h4>Categories:</h4>
1530
+ <ul>
1531
+ ${categories.map(cat => `<li>${cat}</li>`).join('')}
1532
+ </ul>
1533
+ <p>Total Files: ${data.total_raw_files || 0}</p>
1534
+ </div>
1535
+ `;
1536
+ }
1537
+
1538
+ // Also update metadata container if it exists
1539
+ const metadataContainer = document.getElementById('api-registry-metadata');
1540
+ if (metadataContainer) {
1541
+ metadataContainer.innerHTML = `
1542
+ <div class="card">
1543
+ <h4>Metadata</h4>
1544
+ <pre>${JSON.stringify(metadata, null, 2)}</pre>
1545
+ </div>
1546
+ `;
1547
+ }
1548
+ } catch (error) {
1549
+ console.error('Error loading API registry:', error);
1550
+ }
1551
+ }
1552
+