/
/
/
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Music Assistant API - Commands Reference</title>
7 <link rel="stylesheet" href="../resources/common.css">
8 <style>
9 .nav-container {
10 background: var(--panel);
11 padding: 1rem 2rem;
12 box-shadow: 0 2px 5px rgba(0,0,0,0.05);
13 position: sticky;
14 top: 0;
15 z-index: 100;
16 display: flex;
17 flex-direction: column;
18 gap: 1rem;
19 }
20 .search-box input {
21 width: 100%;
22 max-width: 600px;
23 padding: 0.6rem 1rem;
24 font-size: 0.95em;
25 border: 2px solid var(--border);
26 border-radius: 8px;
27 display: block;
28 margin: 0 auto;
29 }
30 .search-box input:focus {
31 outline: none;
32 border-color: var(--primary);
33 }
34 .quick-nav {
35 display: flex;
36 flex-wrap: wrap;
37 gap: 0.5rem;
38 justify-content: center;
39 padding-top: 0.5rem;
40 border-top: 1px solid var(--border);
41 }
42 .quick-nav a {
43 padding: 0.4rem 1rem;
44 background: var(--panel);
45 color: var(--primary);
46 text-decoration: none;
47 border-radius: 6px;
48 font-size: 0.9em;
49 transition: all 0.2s;
50 }
51 .quick-nav a:hover {
52 background: var(--primary);
53 color: var(--fg);
54 }
55 .container {
56 max-width: 1200px;
57 margin: 2rem auto;
58 padding: 0 2rem;
59 }
60 .category {
61 background: var(--panel);
62 margin-bottom: 2rem;
63 border-radius: 12px;
64 box-shadow: 0 2px 10px rgba(0,0,0,0.08);
65 overflow: hidden;
66 }
67 .category-header {
68 background: var(--primary);
69 color: var(--fg);
70 padding: 1rem 1.5rem;
71 font-size: 1.2em;
72 font-weight: 600;
73 cursor: pointer;
74 user-select: none;
75 }
76 .category-header:hover {
77 background: var(--primary);
78 }
79 .command {
80 border-bottom: 1px solid var(--border);
81 }
82 .command:last-child {
83 border-bottom: none;
84 }
85 .command-header {
86 padding: 1rem 1.5rem;
87 cursor: pointer;
88 user-select: none;
89 display: flex;
90 justify-content: space-between;
91 align-items: center;
92 transition: background 0.2s;
93 background: var(--input-bg);
94 }
95 .command-header:hover {
96 background: var(--input-focus-bg);
97 }
98 .command-title {
99 display: flex;
100 flex-direction: column;
101 gap: 0.3rem;
102 flex: 1;
103 }
104 .command-name {
105 font-size: 1.1em;
106 font-weight: 600;
107 color: var(--primary);
108 font-family: 'Monaco', 'Courier New', monospace;
109 }
110 .command-summary {
111 font-size: 0.9em;
112 color: var(--text-secondary);
113 }
114 .command-expand-icon {
115 color: var(--primary);
116 font-size: 1.2em;
117 transition: transform 0.3s;
118 }
119 .command-expand-icon.expanded {
120 transform: rotate(180deg);
121 }
122 .command-details {
123 padding: 0 1.5rem 1.5rem 1.5rem;
124 display: none;
125 }
126 .command-details.show {
127 display: block;
128 }
129 .command-description {
130 color: var(--text-secondary);
131 margin-bottom: 1rem;
132 }
133 .return-type {
134 background: #e8f5e9;
135 padding: 0.5rem 1rem;
136 margin: 1rem 0;
137 border-radius: 6px;
138 border-left: 3px solid #4caf50;
139 }
140 .return-type-label {
141 font-weight: 600;
142 color: #2e7d32;
143 margin-right: 0.5rem;
144 }
145 .return-type-value {
146 font-family: 'Monaco', 'Courier New', monospace;
147 color: #2e7d32;
148 }
149 .params-section {
150 margin: 1rem 0;
151 }
152 .params-title {
153 font-weight: 600;
154 color: #333;
155 margin-bottom: 0.5rem;
156 }
157 .param {
158 background: var(--panel);
159 padding: 0.5rem 1rem;
160 margin: 0.5rem 0;
161 border-radius: 6px;
162 border-left: 3px solid var(--primary);
163 }
164 .param-name {
165 font-family: 'Monaco', 'Courier New', monospace;
166 color: var(--primary);
167 font-weight: 600;
168 }
169 .param-required {
170 color: #e74c3c;
171 font-size: 0.8em;
172 font-weight: 600;
173 margin-left: 0.5rem;
174 }
175 .param-type {
176 color: var(--text-secondary);
177 font-size: 0.9em;
178 margin-left: 0.5rem;
179 }
180 .param-description {
181 color: var(--text-secondary);
182 margin-top: 0.25rem;
183 }
184 .example {
185 background: #2d2d2d;
186 color: #f8f8f2;
187 padding: 1rem;
188 border-radius: 8px;
189 margin: 1rem 0;
190 overflow-x: auto;
191 position: relative;
192 }
193 .example-title {
194 font-weight: 600;
195 color: #333;
196 margin-bottom: 0.5rem;
197 }
198 .example pre {
199 margin: 0;
200 font-family: 'Monaco', 'Courier New', monospace;
201 font-size: 0.9em;
202 }
203 .copy-btn {
204 position: absolute;
205 top: 0.5rem;
206 right: 0.5rem;
207 background: var(--primary);
208 color: var(--fg);
209 border: none;
210 padding: 0.4rem 0.8rem;
211 border-radius: 4px;
212 cursor: pointer;
213 font-size: 0.8em;
214 }
215 .copy-btn:hover {
216 background: var(--primary);
217 }
218 .hidden {
219 display: none;
220 }
221 .tabs {
222 margin: 1rem 0;
223 }
224 .tab-buttons {
225 display: flex;
226 gap: 0.5rem;
227 border-bottom: 2px solid #ddd;
228 margin-bottom: 1rem;
229 }
230 .tab-btn {
231 background: none;
232 border: none;
233 padding: 0.8rem 1.5rem;
234 font-size: 1em;
235 cursor: pointer;
236 color: var(--text-secondary);
237 border-bottom: 3px solid transparent;
238 transition: all 0.3s;
239 }
240 .tab-btn:hover {
241 color: var(--primary);
242 }
243 .tab-btn.active {
244 color: var(--primary);
245 border-bottom-color: var(--primary);
246 }
247 .tab-content {
248 display: none;
249 }
250 .tab-content.active {
251 display: block;
252 }
253 .try-it-section {
254 display: flex;
255 flex-direction: column;
256 gap: 1rem;
257 }
258 .json-input {
259 width: 100%;
260 min-height: 150px;
261 padding: 1rem;
262 font-family: 'Monaco', 'Courier New', monospace;
263 font-size: 0.9em;
264 border: 2px solid var(--border);
265 border-radius: 8px;
266 background: #2d2d2d;
267 color: #f8f8f2;
268 resize: vertical;
269 }
270 .json-input:focus {
271 outline: none;
272 border-color: var(--primary);
273 }
274 .try-btn {
275 align-self: flex-start;
276 background: var(--primary);
277 color: var(--fg);
278 border: none;
279 padding: 0.8rem 2rem;
280 border-radius: 8px;
281 font-size: 1em;
282 cursor: pointer;
283 transition: background 0.3s;
284 }
285 .try-btn:hover {
286 background: var(--primary);
287 }
288 .try-btn:disabled {
289 background: #ccc;
290 cursor: not-allowed;
291 }
292 .response-output {
293 background: #2d2d2d;
294 color: #f8f8f2;
295 padding: 1rem;
296 border-radius: 8px;
297 font-family: 'Monaco', 'Courier New', monospace;
298 font-size: 0.9em;
299 min-height: 100px;
300 white-space: pre-wrap;
301 word-wrap: break-word;
302 display: none;
303 }
304 .response-output.show {
305 display: block;
306 }
307 .response-output.error {
308 background: #ffebee;
309 color: #c62828;
310 }
311 .response-output.success {
312 background: #e8f5e9;
313 color: #2e7d32;
314 }
315 .type-link {
316 color: var(--primary);
317 text-decoration: none;
318 border-bottom: 1px dashed var(--primary);
319 transition: all 0.2s;
320 }
321 .type-link:hover {
322 color: var(--primary);
323 border-bottom-color: var(--primary);
324 }
325 .type-union {
326 margin-top: 0.5rem;
327 }
328 .type-union-label {
329 font-weight: 600;
330 color: #4a5568;
331 display: block;
332 margin-bottom: 0.25rem;
333 }
334 .type-union ul {
335 margin: 0.25rem 0 0 0;
336 padding-left: 1.5rem;
337 list-style-type: disc;
338 }
339 .type-union li {
340 margin: 0.25rem 0;
341 color: #2d3748;
342 }
343 .param-type-union {
344 display: block;
345 margin-top: 0.25rem;
346 }
347 .auth-section {
348 background: var(--panel);
349 padding: 1rem;
350 border-radius: 8px;
351 margin-bottom: 1rem;
352 border: 2px solid var(--border);
353 }
354 .auth-section.authenticated {
355 border-color: var(--success-border);
356 background: var(--success-bg);
357 }
358 .auth-status {
359 display: flex;
360 align-items: center;
361 gap: 0.5rem;
362 margin-bottom: 0.8rem;
363 font-weight: 600;
364 color: var(--fg);
365 }
366 .auth-status-dot {
367 width: 10px;
368 height: 10px;
369 border-radius: 50%;
370 background: #f44336;
371 }
372 .auth-status-dot.authenticated {
373 background: var(--success);
374 }
375 .auth-form {
376 display: flex;
377 flex-direction: column;
378 gap: 0.8rem;
379 }
380 .auth-form input {
381 padding: 0.6rem;
382 border: 2px solid var(--border);
383 border-radius: 6px;
384 font-size: 0.95em;
385 background: var(--panel);
386 color: var(--fg);
387 }
388 .auth-form input:focus {
389 outline: none;
390 border-color: var(--primary);
391 }
392 .auth-form button {
393 padding: 0.6rem 1.2rem;
394 background: var(--primary);
395 color: white;
396 border: none;
397 border-radius: 6px;
398 font-size: 0.95em;
399 cursor: pointer;
400 transition: background 0.3s;
401 }
402 .auth-form button:hover {
403 filter: brightness(1.1);
404 }
405 .auth-form button:disabled {
406 background: #ccc;
407 cursor: not-allowed;
408 }
409 .auth-user-info {
410 display: flex;
411 justify-content: space-between;
412 align-items: center;
413 }
414 .auth-user-details {
415 font-size: 0.9em;
416 color: var(--text-secondary);
417 }
418 .auth-logout-btn {
419 padding: 0.5rem 1rem;
420 background: #f44336;
421 color: white;
422 border: none;
423 border-radius: 6px;
424 font-size: 0.9em;
425 cursor: pointer;
426 transition: background 0.3s;
427 }
428 .auth-logout-btn:hover {
429 filter: brightness(0.9);
430 }
431 .auth-error {
432 background: #ffebee;
433 color: #c62828;
434 padding: 0.6rem;
435 border-radius: 6px;
436 font-size: 0.9em;
437 margin-top: 0.5rem;
438 }
439 .role-badge {
440 display: inline-block;
441 padding: 0.2rem 0.6rem;
442 border-radius: 4px;
443 font-size: 0.75em;
444 font-weight: 600;
445 margin-left: 0.5rem;
446 text-transform: uppercase;
447 }
448 .role-badge.admin {
449 background: #ffebee;
450 color: #c62828;
451 }
452 .role-badge.user {
453 background: #e3f2fd;
454 color: #1976d2;
455 }
456 .header .logo {
457 margin-bottom: 1rem;
458 }
459 .header .logo img {
460 width: 60px;
461 height: 60px;
462 object-fit: contain;
463 }
464 .loading {
465 text-align: center;
466 padding: 2rem;
467 font-size: 1.2em;
468 color: var(--text-secondary);
469 }
470 </style>
471</head>
472<body>
473 <div class="header">
474 <div class="logo">
475 <img src="../logo.png" alt="Music Assistant">
476 </div>
477 <h1>Commands Reference</h1>
478 <p>Complete list of Music Assistant API commands</p>
479 </div>
480
481 <div class="nav-container">
482 <div class="auth-section" id="authSection">
483 <div class="auth-status">
484 <span class="auth-status-dot" id="authDot"></span>
485 <span id="authStatusText">Not Authenticated</span>
486 </div>
487 <div id="authFormContainer">
488 <form class="auth-form" id="loginForm" onsubmit="return handleLogin(event)">
489 <input type="text" id="username" placeholder="Username" required />
490 <input type="password" id="password" placeholder="Password" required />
491 <button type="submit" id="loginBtn">Login</button>
492 </form>
493 <div id="authError" class="auth-error" style="display: none;"></div>
494 </div>
495 <div id="authUserInfo" class="auth-user-info" style="display: none;">
496 <div class="auth-user-details">
497 <div>Logged in as: <strong id="authUsername"></strong></div>
498 <div>Role: <strong id="authRole"></strong></div>
499 </div>
500 <button class="auth-logout-btn" onclick="handleLogout()">Logout</button>
501 </div>
502 </div>
503 <div class="search-box">
504 <input type="text" id="search" placeholder="Search commands..." />
505 </div>
506 <div class="quick-nav" id="quickNav">
507 <!-- Navigation links will be generated dynamically -->
508 </div>
509 </div>
510
511 <div class="container" id="container">
512 <div class="loading">Loading commands...</div>
513 </div>
514
515 <script>
516 // Get server URL from current location
517 const SERVER_URL = window.location.origin;
518
519 // Authentication functionality
520 const TOKEN_STORAGE_KEY = 'ma_api_token';
521 const USER_STORAGE_KEY = 'ma_api_user';
522
523 // Check for existing token on page load
524 async function checkAuth() {
525 const token = localStorage.getItem(TOKEN_STORAGE_KEY);
526 const userStr = localStorage.getItem(USER_STORAGE_KEY);
527
528 if (token && userStr) {
529 try {
530 const user = JSON.parse(userStr);
531
532 // Validate token by making a JSON-RPC call that requires auth
533 const response = await fetch('/api', {
534 method: 'POST',
535 headers: {
536 'Content-Type': 'application/json',
537 'Authorization': `Bearer ${token}`
538 },
539 body: JSON.stringify({
540 command: 'info'
541 })
542 });
543
544 if (response.ok) {
545 // Token is valid
546 updateAuthUI(true, user);
547 } else {
548 // Token is invalid (revoked, expired, etc.)
549 clearAuth();
550 }
551 } catch (e) {
552 // Network error or invalid JSON
553 clearAuth();
554 }
555 }
556 }
557
558 // Handle login form submission
559 async function handleLogin(event) {
560 event.preventDefault();
561
562 const username = document.getElementById('username').value;
563 const password = document.getElementById('password').value;
564 const loginBtn = document.getElementById('loginBtn');
565 const errorDiv = document.getElementById('authError');
566
567 // Disable button and show loading
568 loginBtn.disabled = true;
569 loginBtn.textContent = 'Logging in...';
570 errorDiv.style.display = 'none';
571
572 try {
573 const response = await fetch('/auth/login', {
574 method: 'POST',
575 headers: {
576 'Content-Type': 'application/json',
577 },
578 body: JSON.stringify({
579 credentials: {
580 username: username,
581 password: password
582 }
583 })
584 });
585
586 const result = await response.json();
587
588 if (result.success && result.token && result.user) {
589 // Store token and user info
590 localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
591 localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(result.user));
592
593 // Update UI
594 updateAuthUI(true, result.user);
595
596 // Clear form
597 document.getElementById('loginForm').reset();
598 } else {
599 // Show error
600 errorDiv.textContent = result.error || 'Login failed';
601 errorDiv.style.display = 'block';
602 }
603 } catch (error) {
604 errorDiv.textContent = 'Connection error: ' + error.message;
605 errorDiv.style.display = 'block';
606 } finally {
607 loginBtn.disabled = false;
608 loginBtn.textContent = 'Login';
609 }
610
611 return false;
612 }
613
614 // Handle logout
615 function handleLogout() {
616 clearAuth();
617 }
618
619 // Clear authentication
620 function clearAuth() {
621 localStorage.removeItem(TOKEN_STORAGE_KEY);
622 localStorage.removeItem(USER_STORAGE_KEY);
623 updateAuthUI(false, null);
624 }
625
626 // Update auth UI
627 function updateAuthUI(authenticated, user) {
628 const authSection = document.getElementById('authSection');
629 const authDot = document.getElementById('authDot');
630 const authStatusText = document.getElementById('authStatusText');
631 const authFormContainer = document.getElementById('authFormContainer');
632 const authUserInfo = document.getElementById('authUserInfo');
633
634 if (authenticated && user) {
635 authSection.classList.add('authenticated');
636 authDot.classList.add('authenticated');
637 authStatusText.textContent = 'Authenticated';
638 authFormContainer.style.display = 'none';
639 authUserInfo.style.display = 'flex';
640
641 document.getElementById('authUsername').textContent = user.username;
642 document.getElementById('authRole').textContent = user.role;
643
644 // Update cURL examples with actual token
645 updateCurlExamples();
646 } else {
647 authSection.classList.remove('authenticated');
648 authDot.classList.remove('authenticated');
649 authStatusText.textContent = 'Not Authenticated';
650 authFormContainer.style.display = 'block';
651 authUserInfo.style.display = 'none';
652
653 // Reset cURL examples to placeholder
654 updateCurlExamples();
655 }
656 }
657
658 // Update all cURL examples with actual token or placeholder
659 function updateCurlExamples() {
660 const token = localStorage.getItem(TOKEN_STORAGE_KEY);
661 const curlBlocks = document.querySelectorAll('.example pre');
662
663 curlBlocks.forEach(block => {
664 const curlText = block.textContent;
665
666 // Only update if it contains an Authorization header
667 if (curlText.includes('Authorization: Bearer')) {
668 if (token) {
669 // Replace placeholder with actual token
670 block.textContent = curlText.replace(
671 /Authorization: Bearer YOUR_ACCESS_TOKEN/g,
672 `Authorization: Bearer ${token}`
673 );
674 } else {
675 // Replace actual token with placeholder
676 block.textContent = curlText.replace(
677 /Authorization: Bearer \S+/g,
678 'Authorization: Bearer YOUR_ACCESS_TOKEN'
679 );
680 }
681 }
682 });
683 }
684
685 // Initialize auth on page load
686 checkAuth();
687
688 // Helper function to make type links
689 function makeTypeLinks(typeStr, asList = false) {
690 // Find all complex types (capitalized words that aren't basic types)
691 const excluded = ['Union', 'Optional', 'List', 'Dict', 'Array', 'None', 'NoneType'];
692
693 function replaceType(typeStr) {
694 return typeStr.replace(/\b[A-Z][a-zA-Z0-9_]*\b/g, (match) => {
695 if (match[0] === match[0].toUpperCase() && !excluded.includes(match)) {
696 const schemaUrl = `${SERVER_URL}/api-docs/schemas#schema-${match}`;
697 return `<a href="${schemaUrl}" class="type-link">${match}</a>`;
698 }
699 return match;
700 });
701 }
702
703 // If it's a union type with multiple options and asList is true, format as bullet list
704 if (asList && typeStr.includes(' | ')) {
705 const parts = typeStr.split(' | ');
706 // Only use list format if there are 3+ options
707 if (parts.length >= 3) {
708 let html = '<div class="type-union"><span class="type-union-label">Any of:</span><ul>';
709 for (const part of parts) {
710 const linkedPart = replaceType(part);
711 html += `<li>${linkedPart}</li>`;
712 }
713 html += '</ul></div>';
714 return html;
715 }
716 }
717
718 // Replace complex type names with links
719 return replaceType(typeStr);
720 }
721
722 // Helper function to escape HTML
723 function escapeHtml(text) {
724 const div = document.createElement('div');
725 div.textContent = text;
726 return div.innerHTML;
727 }
728
729 // Helper function to build example args
730 function buildExampleArgs(parameters) {
731 const exampleArgs = {};
732
733 for (const param of parameters) {
734 // Include optional params if few params
735 if (param.required || parameters.length <= 2) {
736 const typeStr = param.type;
737
738 if (typeStr === 'string') {
739 exampleArgs[param.name] = 'example_value';
740 } else if (typeStr === 'integer') {
741 exampleArgs[param.name] = 0;
742 } else if (typeStr === 'number') {
743 exampleArgs[param.name] = 0.0;
744 } else if (typeStr === 'boolean') {
745 exampleArgs[param.name] = true;
746 } else if (typeStr === 'object') {
747 exampleArgs[param.name] = {};
748 } else if (typeStr === 'null') {
749 exampleArgs[param.name] = null;
750 } else if (typeStr === 'ConfigValueType') {
751 exampleArgs[param.name] = 'example_value';
752 } else if (typeStr === 'MediaItemType') {
753 exampleArgs[param.name] = { "_comment": "See MediaItemType schema for details" };
754 } else if (typeStr.startsWith('Array of ')) {
755 const itemType = typeStr.substring(9);
756 if (['string', 'integer', 'number', 'boolean'].includes(itemType)) {
757 exampleArgs[param.name] = [];
758 } else {
759 exampleArgs[param.name] = [{ "_comment": `See ${itemType} schema in Swagger UI` }];
760 }
761 } else {
762 // Complex type - use placeholder object
763 const primaryType = typeStr.includes(' | ') ? typeStr.split(' | ')[0] : typeStr;
764 exampleArgs[param.name] = { "_comment": `See ${primaryType} schema in Swagger UI` };
765 }
766 }
767 }
768
769 return exampleArgs;
770 }
771
772 // Render commands from JSON data
773 function renderCommands(commandsData) {
774 const container = document.getElementById('container');
775 const quickNav = document.getElementById('quickNav');
776
777 // Group commands by category
778 const categories = {};
779 for (const cmd of commandsData) {
780 if (!categories[cmd.category]) {
781 categories[cmd.category] = [];
782 }
783 categories[cmd.category].push(cmd);
784 }
785
786 // Clear container
787 container.innerHTML = '';
788 quickNav.innerHTML = '';
789
790 // Add quick navigation links
791 const sortedCategories = Object.keys(categories).sort();
792 for (const category of sortedCategories) {
793 const categoryId = category.toLowerCase().replace(/\s+/g, '-');
794 const link = document.createElement('a');
795 link.href = `#${categoryId}`;
796 link.textContent = category;
797 quickNav.appendChild(link);
798 }
799
800 // Render each category
801 for (const category of sortedCategories) {
802 const commands = categories[category];
803 const categoryId = category.toLowerCase().replace(/\s+/g, '-');
804
805 const categoryDiv = document.createElement('div');
806 categoryDiv.className = 'category';
807 categoryDiv.id = categoryId;
808 categoryDiv.dataset.category = categoryId;
809
810 const categoryHeader = document.createElement('div');
811 categoryHeader.className = 'category-header';
812 categoryHeader.textContent = category;
813 categoryDiv.appendChild(categoryHeader);
814
815 const categoryContent = document.createElement('div');
816 categoryContent.className = 'category-content';
817
818 // Render each command in category
819 for (const cmd of commands) {
820 const commandDiv = document.createElement('div');
821 commandDiv.className = 'command';
822 commandDiv.dataset.command = cmd.command;
823
824 // Command header
825 const commandHeader = document.createElement('div');
826 commandHeader.className = 'command-header';
827 commandHeader.onclick = function() { toggleCommand(this); };
828
829 const commandTitle = document.createElement('div');
830 commandTitle.className = 'command-title';
831
832 const commandName = document.createElement('div');
833 commandName.className = 'command-name';
834 commandName.textContent = cmd.command;
835
836 // Add role badge if required
837 if (cmd.required_role) {
838 const roleBadge = document.createElement('span');
839 roleBadge.className = `role-badge ${cmd.required_role.toLowerCase()}`;
840 roleBadge.textContent = cmd.required_role;
841 commandName.appendChild(roleBadge);
842 }
843
844 commandTitle.appendChild(commandName);
845
846 if (cmd.summary) {
847 const commandSummary = document.createElement('div');
848 commandSummary.className = 'command-summary';
849 commandSummary.textContent = cmd.summary;
850 commandTitle.appendChild(commandSummary);
851 }
852
853 commandHeader.appendChild(commandTitle);
854
855 const expandIcon = document.createElement('div');
856 expandIcon.className = 'command-expand-icon';
857 expandIcon.textContent = '▼';
858 commandHeader.appendChild(expandIcon);
859
860 commandDiv.appendChild(commandHeader);
861
862 // Command details
863 const commandDetails = document.createElement('div');
864 commandDetails.className = 'command-details';
865
866 // Description
867 if (cmd.description && cmd.description !== cmd.summary) {
868 const descDiv = document.createElement('div');
869 descDiv.className = 'command-description';
870 descDiv.textContent = cmd.description;
871 commandDetails.appendChild(descDiv);
872 }
873
874 // Return type
875 const returnTypeDiv = document.createElement('div');
876 returnTypeDiv.className = 'return-type';
877 returnTypeDiv.innerHTML = `
878 <span class="return-type-label">Returns:</span>
879 <span class="return-type-value">${makeTypeLinks(cmd.return_type)}</span>
880 `;
881 commandDetails.appendChild(returnTypeDiv);
882
883 // Parameters
884 if (cmd.parameters && cmd.parameters.length > 0) {
885 const paramsSection = document.createElement('div');
886 paramsSection.className = 'params-section';
887
888 const paramsTitle = document.createElement('div');
889 paramsTitle.className = 'params-title';
890 paramsTitle.textContent = 'Parameters:';
891 paramsSection.appendChild(paramsTitle);
892
893 for (const param of cmd.parameters) {
894 const paramDiv = document.createElement('div');
895 paramDiv.className = 'param';
896
897 const typeHtml = makeTypeLinks(param.type, true);
898
899 let paramHtml = `<span class="param-name">${escapeHtml(param.name)}</span>`;
900 if (param.required) {
901 paramHtml += '<span class="param-required">REQUIRED</span>';
902 }
903
904 // If it's a list format, display it differently
905 if (typeHtml.includes('<ul>')) {
906 paramHtml += `<div class="param-type-union">${typeHtml}</div>`;
907 } else {
908 paramHtml += `<span class="param-type">${typeHtml}</span>`;
909 }
910
911 if (param.description) {
912 paramHtml += `<div class="param-description">${escapeHtml(param.description)}</div>`;
913 }
914
915 paramDiv.innerHTML = paramHtml;
916 paramsSection.appendChild(paramDiv);
917 }
918
919 commandDetails.appendChild(paramsSection);
920 }
921
922 // Build example request body
923 const exampleArgs = buildExampleArgs(cmd.parameters || []);
924 const requestBody = { command: cmd.command };
925 if (Object.keys(exampleArgs).length > 0) {
926 requestBody.args = exampleArgs;
927 }
928
929 // Build cURL command
930 let curlHeaders = ' -H "Content-Type: application/json"';
931 if (cmd.authenticated) {
932 curlHeaders += ' \\\n -H "Authorization: Bearer YOUR_ACCESS_TOKEN"';
933 }
934
935 const curlCmd = `curl -X POST ${SERVER_URL}/api \\\n${curlHeaders} \\\n -d '${JSON.stringify(requestBody, null, 2)}'`;
936
937 // Add tabs
938 const tabsDiv = document.createElement('div');
939 tabsDiv.className = 'tabs';
940
941 const tabButtons = document.createElement('div');
942 tabButtons.className = 'tab-buttons';
943
944 const curlTabBtn = document.createElement('button');
945 curlTabBtn.className = 'tab-btn active';
946 curlTabBtn.textContent = 'cURL';
947 curlTabBtn.onclick = function() { switchTab(this, `curl-${cmd.command.replace(/\//g, '-')}`); };
948 tabButtons.appendChild(curlTabBtn);
949
950 const tryitTabBtn = document.createElement('button');
951 tryitTabBtn.className = 'tab-btn';
952 tryitTabBtn.textContent = 'Try It';
953 tryitTabBtn.onclick = function() { switchTab(this, `tryit-${cmd.command.replace(/\//g, '-')}`); };
954 tabButtons.appendChild(tryitTabBtn);
955
956 tabsDiv.appendChild(tabButtons);
957
958 // cURL tab
959 const curlTabContent = document.createElement('div');
960 curlTabContent.id = `curl-${cmd.command.replace(/\//g, '-')}`;
961 curlTabContent.className = 'tab-content active';
962
963 const exampleDiv = document.createElement('div');
964 exampleDiv.className = 'example';
965
966 const copyBtn = document.createElement('button');
967 copyBtn.className = 'copy-btn';
968 copyBtn.textContent = 'Copy';
969 copyBtn.onclick = function() { copyCode(this); };
970 exampleDiv.appendChild(copyBtn);
971
972 const pre = document.createElement('pre');
973 pre.textContent = curlCmd;
974 exampleDiv.appendChild(pre);
975
976 curlTabContent.appendChild(exampleDiv);
977 tabsDiv.appendChild(curlTabContent);
978
979 // Try It tab
980 const tryitTabContent = document.createElement('div');
981 tryitTabContent.id = `tryit-${cmd.command.replace(/\//g, '-')}`;
982 tryitTabContent.className = 'tab-content';
983
984 const tryItSection = document.createElement('div');
985 tryItSection.className = 'try-it-section';
986
987 const jsonInput = document.createElement('textarea');
988 jsonInput.className = 'json-input';
989 jsonInput.value = JSON.stringify(requestBody, null, 2);
990 tryItSection.appendChild(jsonInput);
991
992 const tryBtn = document.createElement('button');
993 tryBtn.className = 'try-btn';
994 tryBtn.textContent = 'Execute';
995 tryBtn.onclick = function() { tryCommand(this, cmd.command); };
996 tryItSection.appendChild(tryBtn);
997
998 const responseOutput = document.createElement('div');
999 responseOutput.className = 'response-output';
1000 tryItSection.appendChild(responseOutput);
1001
1002 tryitTabContent.appendChild(tryItSection);
1003 tabsDiv.appendChild(tryitTabContent);
1004
1005 commandDetails.appendChild(tabsDiv);
1006 commandDiv.appendChild(commandDetails);
1007 categoryContent.appendChild(commandDiv);
1008 }
1009
1010 categoryDiv.appendChild(categoryContent);
1011 container.appendChild(categoryDiv);
1012 }
1013
1014 // Update cURL examples if already authenticated
1015 updateCurlExamples();
1016 }
1017
1018 // Load commands from JSON endpoint
1019 async function loadCommands() {
1020 try {
1021 const response = await fetch('/api-docs/commands.json');
1022 if (!response.ok) {
1023 throw new Error('Failed to load commands');
1024 }
1025 const commandsData = await response.json();
1026 renderCommands(commandsData);
1027 } catch (error) {
1028 document.getElementById('container').innerHTML =
1029 `<div class="loading">Error loading commands: ${escapeHtml(error.message)}</div>`;
1030 }
1031 }
1032
1033 // Search functionality
1034 document.getElementById('search').addEventListener('input', function(e) {
1035 const searchTerm = e.target.value.toLowerCase();
1036 const commands = document.querySelectorAll('.command');
1037 const categories = document.querySelectorAll('.category');
1038
1039 commands.forEach(command => {
1040 const commandName = command.dataset.command;
1041 const commandText = command.textContent.toLowerCase();
1042 if (commandName.includes(searchTerm) || commandText.includes(searchTerm)) {
1043 command.classList.remove('hidden');
1044 } else {
1045 command.classList.add('hidden');
1046 }
1047 });
1048
1049 // Hide empty categories
1050 categories.forEach(category => {
1051 const visibleCommands = category.querySelectorAll('.command:not(.hidden)');
1052 if (visibleCommands.length === 0) {
1053 category.classList.add('hidden');
1054 } else {
1055 category.classList.remove('hidden');
1056 }
1057 });
1058 });
1059
1060 // Toggle command details
1061 function toggleCommand(header) {
1062 const command = header.parentElement;
1063 const details = command.querySelector('.command-details');
1064 const icon = header.querySelector('.command-expand-icon');
1065
1066 details.classList.toggle('show');
1067 icon.classList.toggle('expanded');
1068 }
1069
1070 // Copy to clipboard
1071 function copyCode(button) {
1072 const code = button.nextElementSibling.textContent;
1073 navigator.clipboard.writeText(code).then(() => {
1074 const originalText = button.textContent;
1075 button.textContent = 'Copied!';
1076 setTimeout(() => {
1077 button.textContent = originalText;
1078 }, 2000);
1079 });
1080 }
1081
1082 // Tab switching
1083 function switchTab(button, tabId) {
1084 const tabButtons = button.parentElement;
1085 const tabs = tabButtons.parentElement;
1086
1087 // Remove active class from all buttons and tabs
1088 tabButtons.querySelectorAll('.tab-btn').forEach(btn => {
1089 btn.classList.remove('active');
1090 });
1091 tabs.querySelectorAll('.tab-content').forEach(content => {
1092 content.classList.remove('active');
1093 });
1094
1095 // Add active class to clicked button and corresponding tab
1096 button.classList.add('active');
1097 document.getElementById(tabId).classList.add('active');
1098 }
1099
1100 // Try command functionality
1101 async function tryCommand(button, commandName) {
1102 const section = button.parentElement;
1103 const textarea = section.querySelector('.json-input');
1104 const output = section.querySelector('.response-output');
1105
1106 // Disable button while processing
1107 button.disabled = true;
1108 button.textContent = 'Executing...';
1109
1110 // Clear previous output
1111 output.className = 'response-output show';
1112 output.textContent = 'Loading...';
1113
1114 try {
1115 // Parse JSON from textarea
1116 let requestBody;
1117 try {
1118 requestBody = JSON.parse(textarea.value);
1119 } catch (e) {
1120 throw new Error('Invalid JSON: ' + e.message);
1121 }
1122
1123 // Get stored token
1124 const token = localStorage.getItem(TOKEN_STORAGE_KEY);
1125
1126 // Build headers
1127 const headers = {
1128 'Content-Type': 'application/json',
1129 };
1130
1131 // Add authorization header if token exists
1132 if (token) {
1133 headers['Authorization'] = 'Bearer ' + token;
1134 }
1135
1136 // Make API request
1137 const response = await fetch('/api', {
1138 method: 'POST',
1139 headers: headers,
1140 body: JSON.stringify(requestBody)
1141 });
1142
1143 let result;
1144 const contentType = response.headers.get('content-type');
1145 if (contentType && contentType.includes('application/json')) {
1146 result = await response.json();
1147 } else {
1148 const text = await response.text();
1149 result = { error: text };
1150 }
1151
1152 // Display result
1153 if (response.ok) {
1154 output.className = 'response-output show success';
1155 output.textContent = 'Success!\n\n' + JSON.stringify(result, null, 2);
1156 } else {
1157 output.className = 'response-output show error';
1158
1159 // Handle 401/403 - token invalid or revoked
1160 if (response.status === 401 || response.status === 403) {
1161 clearAuth();
1162 output.textContent = 'Authentication Error: Your session has expired '
1163 + 'or token was revoked. Please login again.';
1164 } else {
1165 // Try to extract a meaningful error message
1166 let errorMsg = 'Request failed';
1167 if (result.error) {
1168 errorMsg = result.error;
1169 } else if (result.message) {
1170 errorMsg = result.message;
1171 } else if (typeof result === 'string') {
1172 errorMsg = result;
1173 } else {
1174 errorMsg = JSON.stringify(result, null, 2);
1175 }
1176 output.textContent = 'Error: ' + errorMsg;
1177 }
1178 }
1179 } catch (error) {
1180 output.className = 'response-output show error';
1181 // Provide more user-friendly error messages
1182 if (error.message.includes('Invalid JSON')) {
1183 output.textContent = 'JSON Syntax Error: Please check your request format. '
1184 + error.message;
1185 } else if (error.message.includes('Failed to fetch')) {
1186 output.textContent = 'Connection Error: Unable to reach the API server. '
1187 + 'Please check if the server is running.';
1188 } else {
1189 output.textContent = 'Error: ' + error.message;
1190 }
1191 } finally {
1192 button.disabled = false;
1193 button.textContent = 'Execute';
1194 }
1195 }
1196
1197 // Load commands on page load
1198 loadCommands();
1199 </script>
1200</body>
1201</html>
1202