a fancy canvas mcp server!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 617 lines 20 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Dashboard - Canvas MCP</title> 8 <meta name="description" 9 content="Manage your Canvas MCP Server connection and API credentials. Connect your Canvas LMS to AI assistants."> 10 <link rel="icon" type="image/x-icon" href="./favicon.ico"> 11 <link rel="canonical" href="https://canvas.dunkirk.sh/dashboard" id="canonical-url"> 12 <meta name="theme-color" content="#0066cc"> 13 14 <!-- Open Graph / Facebook --> 15 <meta property="og:type" content="website"> 16 <meta property="og:url" content="https://canvas.dunkirk.sh/dashboard" id="og-url"> 17 <meta property="og:title" content="Dashboard - Canvas MCP"> 18 <meta property="og:site_name" content="Canvas MCP Server"> 19 <meta property="og:description" 20 content="Manage your Canvas MCP Server connection and API credentials. Connect your Canvas LMS to AI assistants."> 21 <meta property="og:image" content="https://canvas.dunkirk.sh/og.png" id="og-image"> 22 <meta property="og:image:width" content="1200"> 23 <meta property="og:image:height" content="630"> 24 <meta property="og:image:alt" content="Canvas MCP Server - Connect Canvas LMS to AI assistants"> 25 26 <!-- Twitter --> 27 <meta property="twitter:card" content="summary_large_image"> 28 <meta property="twitter:url" content="https://canvas.dunkirk.sh/dashboard" id="twitter-url"> 29 <meta property="twitter:title" content="Dashboard - Canvas MCP"> 30 <meta property="twitter:description" 31 content="Manage your Canvas MCP Server connection and API credentials. Connect your Canvas LMS to AI assistants."> 32 <meta property="twitter:image" content="https://canvas.dunkirk.sh/og.png" id="twitter-image"> 33 34 <script> 35 // Set dynamic URLs based on current host 36 const baseUrl = window.location.origin; 37 document.getElementById('canonical-url').setAttribute('href', `${baseUrl}/dashboard`); 38 document.getElementById('og-url').setAttribute('content', `${baseUrl}/dashboard`); 39 document.getElementById('og-image').setAttribute('content', `${baseUrl}/og.png`); 40 document.getElementById('twitter-url').setAttribute('content', `${baseUrl}/dashboard`); 41 document.getElementById('twitter-image').setAttribute('content', `${baseUrl}/og.png`); 42 </script> 43 <style> 44 * { 45 margin: 0; 46 padding: 0; 47 box-sizing: border-box; 48 } 49 50 body { 51 font-family: system-ui, -apple-system, sans-serif; 52 line-height: 1.6; 53 max-width: 900px; 54 margin: 2rem auto; 55 padding: 2rem; 56 color: #111; 57 } 58 59 header { 60 display: flex; 61 justify-content: space-between; 62 align-items: center; 63 margin-bottom: 2rem; 64 padding-bottom: 1rem; 65 border-bottom: 1px solid #ddd; 66 } 67 68 h1 { 69 font-size: 1.75rem; 70 font-weight: 600; 71 } 72 73 button { 74 padding: 0.5rem 1rem; 75 background: #333; 76 color: white; 77 border: none; 78 border-radius: 4px; 79 font-size: 0.9rem; 80 cursor: pointer; 81 } 82 83 button:hover { 84 background: #111; 85 } 86 87 .logout-btn { 88 background: #c33; 89 } 90 91 .logout-btn:hover { 92 background: #a22; 93 } 94 95 section { 96 margin: 2rem 0; 97 padding: 1.5rem; 98 border: 1px solid #ddd; 99 border-radius: 4px; 100 } 101 102 h2 { 103 font-size: 1.25rem; 104 margin-bottom: 1rem; 105 font-weight: 600; 106 } 107 108 .info-grid { 109 display: grid; 110 gap: 0.75rem; 111 } 112 113 .info-row { 114 display: grid; 115 grid-template-columns: 150px 1fr; 116 padding: 0.5rem 0; 117 border-bottom: 1px solid #eee; 118 } 119 120 .info-row:last-child { 121 border-bottom: none; 122 } 123 124 .label { 125 font-weight: 500; 126 color: #555; 127 } 128 129 .value { 130 color: #111; 131 font-family: monospace; 132 font-size: 0.95rem; 133 } 134 135 .api-key-section { 136 background: #f9f9f9; 137 padding: 1rem; 138 border-radius: 4px; 139 } 140 141 .api-key-display { 142 display: flex; 143 gap: 0.5rem; 144 margin: 1rem 0; 145 } 146 147 .api-key-value { 148 flex: 1; 149 padding: 0.75rem; 150 background: white; 151 border: 1px solid #ddd; 152 border-radius: 4px; 153 font-family: monospace; 154 font-size: 0.9rem; 155 overflow-x: auto; 156 white-space: nowrap; 157 } 158 159 .api-key-value:hover { 160 background: #f8f9fa; 161 } 162 163 .api-key-value.hidden { 164 filter: blur(6px); 165 user-select: none; 166 } 167 168 #mcpUrlValue { 169 user-select: all; 170 } 171 172 .btn-group { 173 display: flex; 174 gap: 0.5rem; 175 flex-wrap: wrap; 176 } 177 178 .config-block { 179 background: #2d2d2d; 180 color: #f0f0f0; 181 padding: 1rem; 182 border-radius: 4px; 183 font-family: monospace; 184 font-size: 0.85rem; 185 overflow-x: auto; 186 margin-top: 1rem; 187 white-space: pre; 188 } 189 190 .hidden { 191 display: none; 192 } 193 194 .notification { 195 position: fixed; 196 bottom: 2rem; 197 right: 2rem; 198 background: #2d2d2d; 199 color: white; 200 padding: 1rem 1.5rem; 201 border-radius: 4px; 202 opacity: 0; 203 transition: opacity 0.3s; 204 pointer-events: none; 205 } 206 207 .notification.show { 208 opacity: 1; 209 } 210 211 .stat-grid { 212 display: grid; 213 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 214 gap: 1rem; 215 margin-top: 1rem; 216 } 217 218 .stat { 219 text-align: center; 220 padding: 1rem; 221 background: #f9f9f9; 222 border-radius: 4px; 223 } 224 225 .stat-value { 226 font-size: 2rem; 227 font-weight: bold; 228 color: #0066cc; 229 } 230 231 .stat-label { 232 color: #666; 233 font-size: 0.9rem; 234 margin-top: 0.25rem; 235 } 236 </style> 237</head> 238 239<body> 240 <header> 241 <h1>Dashboard</h1> 242 <button class="logout-btn" id="logoutBtn">Logout</button> 243 </header> 244 245 <section id="connectCanvasSection" style="display: none;"> 246 <h2>Connect Canvas Account</h2> 247 <p style="color: #666; margin-bottom: 1.5rem;"> 248 Connect your Canvas account to start using the MCP server with Claude. 249 </p> 250 <form id="connectCanvasForm"> 251 <div style="margin-bottom: 1rem;"> 252 <label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Canvas Domain</label> 253 <input type="text" id="canvasDomain" placeholder="canvas.university.edu" 254 style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem;" 255 required /> 256 </div> 257 <div style="margin-bottom: 1rem;"> 258 <label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Personal Access Token</label> 259 <input type="password" id="accessToken" placeholder="Get this from Canvas → Settings → New Access Token" 260 style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem;" 261 required /> 262 </div> 263 <button type="submit" style="width: 100%; padding: 0.75rem;">Connect Canvas</button> 264 <div id="connectError" 265 style="display: none; margin-top: 1rem; padding: 0.75rem; background: #fee; border: 1px solid #fcc; border-radius: 4px; color: #c33;"> 266 </div> 267 </form> 268 <details style="margin-top: 1.5rem;"> 269 <summary style="cursor: pointer; color: #666;">How to get a Personal Access Token</summary> 270 <ol style="margin-top: 1rem; padding-left: 1.5rem; color: #666; font-size: 0.9rem;"> 271 <li>Log in to your Canvas account</li> 272 <li>Go to Account → Settings</li> 273 <li>Scroll to "Approved Integrations"</li> 274 <li>Click "+ New Access Token"</li> 275 <li>Set Purpose: "MCP Server"</li> 276 <li>Click "Generate Token"</li> 277 <li>Copy the token and paste it above</li> 278 </ol> 279 </details> 280 </section> 281 282 <section id="accountSection" style="display: none;"> 283 <h2>Account Information</h2> 284 <div class="info-grid" id="accountInfo"> 285 <div class="info-row"> 286 <span class="label">Loading...</span> 287 </div> 288 </div> 289 </section> 290 291 <section id="mcpConnectionSection" style="display: none;"> 292 <h2>MCP Server Connection</h2> 293 <p style="color: #666; margin-bottom: 1.5rem;"> 294 Use these credentials to connect your MCP client (Claude Desktop, Poke, etc.) to your Canvas account. 295 </p> 296 297 <!-- MCP Endpoint URL --> 298 <div style="margin-bottom: 1.5rem;"> 299 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;"> 300 <label style="font-weight: 600; color: #111;">MCP Server URL</label> 301 <button id="copyUrlBtn" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Copy URL</button> 302 </div> 303 <div class="api-key-value" id="mcpUrlValue"> 304 <span id="mcpUrl"></span> 305 </div> 306 <p style="font-size: 0.85rem; color: #666; margin-top: 0.5rem;"> 307 This is your MCP server endpoint URL. 308 </p> 309 </div> 310 311 <!-- API Token --> 312 <div class="api-key-section"> 313 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;"> 314 <label style="font-weight: 600; color: #111;">API Token</label> 315 <button id="regenerateBtn" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Regenerate Token</button> 316 </div> 317 318 <div id="apiKeyDisplay" style="display: none;"> 319 <div 320 style="background: #fff3cd; border: 1px solid #ffc107; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;"> 321 <strong>⚠️ Save this token now!</strong> You won't be able to see it again after leaving this page. 322 </div> 323 <div class="api-key-display"> 324 <div class="api-key-value" id="apiKeyValue"></div> 325 <button id="copyKeyBtn">Copy Token</button> 326 </div> 327 </div> 328 329 <div id="apiKeyHidden" style="display: none;"> 330 <div class="api-key-display"> 331 <div class="api-key-value" style="background: #f5f5f5; color: #999; user-select: none;"> 332 •••••••••••••••••••••••••••••••••••••••• 333 </div> 334 </div> 335 <p style="color: #666; margin-top: 0.5rem; font-size: 0.85rem;"> 336 Your token is hidden for security. Click "Regenerate Token" above to create a new one. 337 </p> 338 </div> 339 </div> 340 </section> 341 342 <section id="quickSetupSection" style="display: none;"> 343 <h2>Quick Setup</h2> 344 <p style="color: #666; margin-bottom: 1rem;"> 345 Add the MCP url to the claude connections page as a custom connection and then authorize it. Poke and some other 346 clients require both the api key and the mcp url. 347 </p> 348 </section> 349 350 <section id="usageStatsSection" style="display: none;"> 351 <h2>Usage Statistics</h2> 352 <div class="stat-grid" id="usageStats"> 353 <div class="stat"> 354 <div class="stat-value">-</div> 355 <div class="stat-label">Loading...</div> 356 </div> 357 </div> 358 </section> 359 360 <div class="notification" id="notification"></div> 361 362 <footer style="margin-top: 4rem; padding-top: 2rem; border-top: 1px solid #ddd; font-size: 0.9rem;"> 363 <div style="display: flex; justify-content: space-between; align-items: center;"> 364 <span style="color: #666;">made by <a href="https://dunkirk.sh" style="color: #666; text-decoration: none;">kieran 365 klukas</a></span> 366 <a id="git-hash-link" href="#" 367 style="color: #999; text-decoration: none; font-family: monospace; font-size: 0.85rem;">...</a> 368 </div> 369 </footer> 370 371 <script type="module"> 372 // Load git hash 373 fetch('/api/version') 374 .then(r => r.json()) 375 .then(data => { 376 const link = document.getElementById('git-hash-link'); 377 if (link) { 378 link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`; 379 link.textContent = data.shortHash; 380 } 381 }) 382 .catch(() => { }); 383 384 let userData = null; 385 let apiKeyVisible = false; 386 387 async function loadDashboard() { 388 try { 389 const response = await fetch('/api/user/me', { 390 credentials: 'include' 391 }); 392 393 if (!response.ok) { 394 window.location.href = '/'; 395 return; 396 } 397 398 userData = await response.json(); 399 400 // Check if Canvas is connected 401 if (!userData.canvas_domain) { 402 // Show Canvas connection form, hide everything else 403 document.getElementById('connectCanvasSection').style.display = 'block'; 404 document.getElementById('accountSection').style.display = 'none'; 405 document.getElementById('mcpConnectionSection').style.display = 'none'; 406 document.getElementById('quickSetupSection').style.display = 'none'; 407 document.getElementById('usageStatsSection').style.display = 'none'; 408 } else { 409 // Show everything when Canvas is connected 410 document.getElementById('connectCanvasSection').style.display = 'none'; 411 document.getElementById('accountSection').style.display = 'block'; 412 document.getElementById('mcpConnectionSection').style.display = 'block'; 413 document.getElementById('quickSetupSection').style.display = 'block'; 414 document.getElementById('usageStatsSection').style.display = 'block'; 415 renderAccountInfo(); 416 renderUsageStats(); 417 setupApiKeyDisplay(); 418 } 419 } catch (error) { 420 console.error('Failed to load dashboard:', error); 421 window.location.href = '/'; 422 } 423 } 424 425 function renderAccountInfo() { 426 const container = document.getElementById('accountInfo'); 427 container.innerHTML = ` 428 <div class="info-row"> 429 <span class="label">Canvas Domain</span> 430 <span class="value">${userData.canvas_domain}</span> 431 </div> 432 <div class="info-row"> 433 <span class="label">Email</span> 434 <span class="value">${userData.email || 'Not provided'}</span> 435 </div> 436 <div class="info-row"> 437 <span class="label">Created</span> 438 <span class="value">${new Date(userData.created_at).toLocaleDateString()}</span> 439 </div> 440 <div class="info-row"> 441 <span class="label">Last Used</span> 442 <span class="value">${userData.last_used_at ? new Date(userData.last_used_at).toLocaleDateString() : 'Never'}</span> 443 </div> 444 `; 445 } 446 447 function renderUsageStats() { 448 const container = document.getElementById('usageStats'); 449 const stats = userData.usage_stats || {}; 450 container.innerHTML = ` 451 <div class="stat"> 452 <div class="stat-value">${stats.total_requests || 0}</div> 453 <div class="stat-label">Total Requests</div> 454 </div> 455 <div class="stat"> 456 <div class="stat-value">${stats.requests_24h || 0}</div> 457 <div class="stat-label">Last 24 Hours</div> 458 </div> 459 <div class="stat"> 460 <div class="stat-value">${stats.requests_7d || 0}</div> 461 <div class="stat-label">Last 7 Days</div> 462 </div> 463 `; 464 } 465 466 function showNotification(message) { 467 const notification = document.getElementById('notification'); 468 notification.textContent = message; 469 notification.classList.add('show'); 470 setTimeout(() => { 471 notification.classList.remove('show'); 472 }, 2000); 473 } 474 475 function setupApiKeyDisplay() { 476 // Always show the MCP URL 477 const mcpUrl = `${window.location.origin}/mcp`; 478 document.getElementById('mcpUrl').textContent = mcpUrl; 479 480 if (userData.api_key) { 481 // Show the key (first time only) 482 document.getElementById('apiKeyDisplay').style.display = 'block'; 483 document.getElementById('apiKeyValue').textContent = userData.api_key; 484 apiKeyVisible = true; 485 } else { 486 // Hide the key 487 document.getElementById('apiKeyHidden').style.display = 'block'; 488 } 489 } 490 491 document.getElementById('copyUrlBtn').addEventListener('click', async () => { 492 const mcpUrl = `${window.location.origin}/mcp`; 493 await navigator.clipboard.writeText(mcpUrl); 494 showNotification('MCP URL copied to clipboard'); 495 }); 496 497 document.getElementById('copyKeyBtn').addEventListener('click', async () => { 498 await navigator.clipboard.writeText(userData.api_key); 499 showNotification('API token copied to clipboard'); 500 }); 501 502 let regenerateState = 'initial'; // 'initial', 'confirm', 'show-token' 503 let newToken = null; 504 505 document.getElementById('regenerateBtn').addEventListener('click', async () => { 506 const btn = document.getElementById('regenerateBtn'); 507 508 if (regenerateState === 'initial') { 509 // First click: show confirmation 510 regenerateState = 'confirm'; 511 btn.textContent = 'Click again to confirm'; 512 btn.style.background = '#c33'; 513 514 // Reset after 3 seconds if not confirmed 515 setTimeout(() => { 516 if (regenerateState === 'confirm') { 517 regenerateState = 'initial'; 518 btn.textContent = 'Regenerate Token'; 519 btn.style.background = ''; 520 } 521 }, 3000); 522 } else if (regenerateState === 'confirm') { 523 // Second click: regenerate 524 btn.disabled = true; 525 btn.textContent = 'Regenerating...'; 526 527 try { 528 const response = await fetch('/api/user/regenerate-key', { 529 method: 'POST', 530 credentials: 'include' 531 }); 532 533 const data = await response.json(); 534 userData.api_key = data.api_key; 535 newToken = data.api_key; 536 537 // Show the new key 538 document.getElementById('apiKeyHidden').style.display = 'none'; 539 document.getElementById('apiKeyDisplay').style.display = 'block'; 540 document.getElementById('apiKeyValue').textContent = userData.api_key; 541 apiKeyVisible = true; 542 543 regenerateState = 'show-token'; 544 btn.disabled = false; 545 btn.textContent = 'Click to copy new token'; 546 btn.style.background = '#0066cc'; 547 } catch (error) { 548 regenerateState = 'initial'; 549 btn.disabled = false; 550 btn.textContent = 'Regenerate Token'; 551 btn.style.background = ''; 552 showNotification('Failed to regenerate token'); 553 } 554 } else if (regenerateState === 'show-token') { 555 // Third click: copy token 556 await navigator.clipboard.writeText(newToken); 557 showNotification('New token copied to clipboard'); 558 559 // Reset button 560 regenerateState = 'initial'; 561 btn.textContent = 'Regenerate Token'; 562 btn.style.background = ''; 563 } 564 }); 565 566 document.getElementById('logoutBtn').addEventListener('click', async () => { 567 await fetch('/api/auth/logout', { 568 method: 'POST', 569 credentials: 'include' 570 }); 571 window.location.href = '/'; 572 }); 573 574 document.getElementById('connectCanvasForm')?.addEventListener('submit', async (e) => { 575 e.preventDefault(); 576 577 const domain = document.getElementById('canvasDomain').value.trim(); 578 const token = document.getElementById('accessToken').value.trim(); 579 const errorDiv = document.getElementById('connectError'); 580 const submitBtn = e.target.querySelector('button[type="submit"]'); 581 582 errorDiv.style.display = 'none'; 583 submitBtn.disabled = true; 584 submitBtn.textContent = 'Connecting...'; 585 586 try { 587 const response = await fetch('/api/auth/token-login', { 588 method: 'POST', 589 headers: {'Content-Type': 'application/json'}, 590 credentials: 'include', 591 body: JSON.stringify({ 592 canvas_domain: domain, 593 access_token: token 594 }) 595 }); 596 597 const data = await response.json(); 598 599 if (!response.ok) { 600 throw new Error(data.error || 'Failed to connect Canvas'); 601 } 602 603 // Reload dashboard to show connected state 604 window.location.reload(); 605 } catch (error) { 606 errorDiv.textContent = error.message; 607 errorDiv.style.display = 'block'; 608 submitBtn.disabled = false; 609 submitBtn.textContent = 'Connect Canvas'; 610 } 611 }); 612 613 loadDashboard(); 614 </script> 615</body> 616 617</html>