music-assistant-server

43.5 KBHTML
commands_reference.html
43.5 KB1,202 lines • xml
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