Download templates/dashboard.html from mistpe/Manager: direct link, hf CLI and curl.
- Browser
- Download file 24.5 kB
-
https://huggingface.co/spaces/mistpe/Manager/resolve/main/templates/dashboard.html
- Command line
-
hf download hf://spaces/mistpe/Manager/templates/dashboard.html
-
curl -L -o dashboard.html https://huggingface.co/spaces/mistpe/Manager/resolve/main/templates/dashboard.html
24.5 kB
| <html lang="zh"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HF Space Manager - 控制面板</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #00ff9d; | |
| --primary-dark: #00cc7d; | |
| --background-color: #0a0b0f; | |
| --card-background: #12141c; | |
| --text-primary: #ffffff; | |
| --text-secondary: #7f8ea3; | |
| --border-color: #1e2029; | |
| --success-color: #00ff9d; | |
| --warning-color: #ff9d00; | |
| --danger-color: #ff2d55; | |
| --sleeping-color: #00ffff; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| } | |
| body { | |
| background-color: var(--background-color); | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(0, 255, 157, 0.03) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(255, 0, 255, 0.03) 0%, transparent 20%); | |
| min-height: 100vh; | |
| color: var(--text-primary); | |
| } | |
| .header { | |
| background: rgba(18, 20, 28, 0.95); | |
| backdrop-filter: blur(10px); | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| z-index: 1000; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .header-content { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .nav-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 2rem; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .logo i { | |
| color: var(--primary-color); | |
| text-shadow: 0 0 10px var(--primary-color); | |
| } | |
| .search-bar { | |
| position: relative; | |
| width: 300px; | |
| } | |
| .search-bar input { | |
| width: 100%; | |
| padding: 0.5rem 1rem 0.5rem 2.5rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border-color); | |
| border-radius: 6px; | |
| color: var(--text-primary); | |
| font-size: 0.9rem; | |
| } | |
| .search-bar i { | |
| position: absolute; | |
| left: 0.8rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-secondary); | |
| } | |
| .user-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .theme-toggle { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| } | |
| .theme-toggle:hover { | |
| color: var(--primary-color); | |
| background: rgba(0, 255, 157, 0.1); | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 80px auto 0; | |
| padding: 2rem; | |
| } | |
| .dashboard-header { | |
| margin-bottom: 2rem; | |
| padding: 1.5rem; | |
| background: var(--card-background); | |
| border-radius: 12px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .stat-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| transition: all 0.3s ease; | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-2px); | |
| border-color: var(--primary-color); | |
| } | |
| .stat-value { | |
| font-size: 2rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: var(--primary-color); | |
| } | |
| .stat-label { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .owner-section { | |
| background: var(--card-background); | |
| border-radius: 12px; | |
| margin-bottom: 2rem; | |
| border: 1px solid var(--border-color); | |
| overflow: hidden; | |
| } | |
| .owner-header { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| .owner-name { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .status-stats { | |
| display: flex; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| } | |
| .status-badge { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 6px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .space-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); | |
| gap: 1.5rem; | |
| padding: 1.5rem; | |
| } | |
| .space-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border-radius: 10px; | |
| border: 1px solid var(--border-color); | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .space-card:hover { | |
| transform: translateY(-2px); | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 20px rgba(0, 255, 157, 0.1); | |
| } | |
| .space-header { | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.02); | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .space-content { | |
| padding: 1rem; | |
| } | |
| .space-info { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .info-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .info-label { | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .info-value { | |
| color: var(--text-primary); | |
| font-size: 0.9rem; | |
| } | |
| .space-metrics { | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.02); | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| justify-content: space-around; | |
| } | |
| .metric-item { | |
| text-align: center; | |
| } | |
| .metric-value { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| .metric-label { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| .action-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 0.75rem; | |
| padding: 1rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| .action-button { | |
| padding: 0.75rem; | |
| border-radius: 6px; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 1px solid var(--border-color); | |
| background: transparent; | |
| color: var(--text-primary); | |
| text-decoration: none; | |
| } | |
| .action-button:hover { | |
| border-color: var(--primary-color); | |
| background: rgba(0, 255, 157, 0.1); | |
| } | |
| .action-button.restart { | |
| border-color: var(--primary-color); | |
| color: var(--primary-color); | |
| } | |
| .action-button.restart:hover { | |
| background: var(--primary-color); | |
| color: var(--background-color); | |
| } | |
| .status-RUNNING { | |
| background: rgba(0, 255, 157, 0.1); | |
| border: 1px solid var(--success-color); | |
| color: var(--success-color); | |
| } | |
| .status-BUILDING { | |
| background: rgba(255, 157, 0, 0.1); | |
| border: 1px solid var(--warning-color); | |
| color: var(--warning-color); | |
| } | |
| .status-SLEEPING { | |
| background: rgba(0, 255, 255, 0.1); | |
| border: 1px solid var(--sleeping-color); | |
| color: var(--sleeping-color); | |
| } | |
| .status-STOPPED { | |
| background: rgba(127, 142, 163, 0.1); | |
| border: 1px solid var(--text-secondary); | |
| color: var(--text-secondary); | |
| } | |
| .status-FAILED { | |
| background: rgba(255, 45, 85, 0.1); | |
| border: 1px solid var(--danger-color); | |
| color: var(--danger-color); | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(10, 11, 15, 0.8); | |
| backdrop-filter: blur(5px); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 9999; | |
| } | |
| .loading-spinner { | |
| position: relative; | |
| width: 60px; | |
| height: 60px; | |
| } | |
| .loading-spinner::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| border: 3px solid var(--border-color); | |
| border-top-color: var(--primary-color); | |
| animation: spin 1s infinite linear; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| @media (max-width: 768px) { | |
| .header-content { | |
| flex-direction: column; | |
| gap: 1rem; | |
| padding: 1rem; | |
| } | |
| .nav-section { | |
| width: 100%; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .search-bar { | |
| width: 100%; | |
| } | |
| .container { | |
| padding: 1rem; | |
| } | |
| .stats-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .space-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .space-info { | |
| grid-template-columns: 1fr; | |
| } | |
| .status-stats { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading" class="loading-overlay"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| <header class="header"> | |
| <div class="header-content"> | |
| <div class="nav-section"> | |
| <div class="logo"> | |
| <i class="fas fa-server"></i> | |
| HF Space Manager | |
| </div> | |
| <div class="search-bar"> | |
| <i class="fas fa-search"></i> | |
| <input type="text" placeholder="搜索 Spaces..." id="spaceSearch"> | |
| </div> | |
| </div> | |
| <div class="user-section"> | |
| <button class="theme-toggle" title="切换主题"> | |
| <i class="fas fa-moon"></i> | |
| </button> | |
| <a href="/logout" class="action-button"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| 退出 | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="container"> | |
| {% if spaces %} | |
| {% set grouped_spaces = {} %} | |
| {% for space in spaces %} | |
| {% if space.owner not in grouped_spaces %} | |
| {% set _ = grouped_spaces.update({space.owner: []}) %} | |
| {% endif %} | |
| {% set _ = grouped_spaces[space.owner].append(space) %} | |
| {% endfor %} | |
| <div class="dashboard-header"> | |
| <div class="stats-grid"> | |
| {% set total_spaces = spaces|length %} | |
| {% set running_spaces = spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %} | |
| {% set sleeping_spaces = spaces|selectattr('status', 'equalto', 'SLEEPING')|list|length %} | |
| {% set stopped_spaces = spaces|selectattr('status', 'equalto', 'STOPPED')|list|length %} | |
| {% set failed_spaces = spaces|selectattr('status', 'equalto', 'FAILED')|list|length %} | |
| <div class="stat-card"> | |
| <div class="stat-value">{{ total_spaces }}</div> | |
| <div class="stat-label">总空间数</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">{{ running_spaces }}</div> | |
| <div class="stat-label">运行中</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">{{ sleeping_spaces }}</div> | |
| <div class="stat-label">休眠中</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">{{ stopped_spaces }}</div> | |
| <div class="stat-label">已停止</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">{{ failed_spaces }}</div> | |
| <div class="stat-label">运行失败</div> | |
| </div> | |
| </div> | |
| </div> | |
| {% for owner, owner_spaces in grouped_spaces.items() %} | |
| {% set running_count = owner_spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %} | |
| {% set building_count = owner_spaces|selectattr('status', 'equalto', 'BUILDING')|list|length %} | |
| {% set sleeping_count = owner_spaces|selectattr('status', 'equalto', 'SLEEPING')|list|length %} | |
| {% set stopped_count = owner_spaces|selectattr('status', 'equalto', 'STOPPED')|list|length %} | |
| {% set failed_count = owner_spaces|selectattr('status', 'equalto', 'FAILED')|list|length %} | |
| <div class="owner-section"> | |
| <div class="owner-header"> | |
| <div class="owner-name"> | |
| <i class="fas fa-user-circle"></i> | |
| {{ owner }} | |
| </div> | |
| <div class="status-stats"> | |
| <span class="status-badge status-RUNNING"> | |
| <i class="fas fa-play-circle"></i> | |
| 运行中: {{ running_count }} | |
| </span> | |
| <span class="status-badge status-SLEEPING"> | |
| <i class="fas fa-moon"></i> | |
| 休眠: {{ sleeping_count }} | |
| </span> | |
| <span class="status-badge status-STOPPED"> | |
| <i class="fas fa-stop-circle"></i> | |
| 停止: {{ stopped_count }} | |
| </span> | |
| <span class="status-badge status-FAILED"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| 失败: {{ failed_count }} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="space-grid"> | |
| {% for space in owner_spaces %} | |
| <div class="space-card" data-space-id="{{ space.repo_id }}"> | |
| <div class="space-header"> | |
| <div class="space-name"> | |
| <i class="fas fa-cube"></i> | |
| {{ space.name }} | |
| </div> | |
| <span class="status-badge status-{{ space.status }}"> | |
| <i class="fas fa-circle"></i> | |
| {{ space.status }} | |
| </span> | |
| </div> | |
| <div class="space-content"> | |
| <div class="space-info"> | |
| <div class="info-item"> | |
| <span class="info-label">Space ID</span> | |
| <span class="info-value">{{ space.repo_id }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">创建时间</span> | |
| <span class="info-value">{{ space.created_at }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">最后修改</span> | |
| <span class="info-value">{{ space.last_modified }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">应用端口</span> | |
| <span class="info-value">{{ space.app_port }}</span> | |
| </div> | |
| </div> | |
| <div class="space-metrics"> | |
| <div class="metric-item"> | |
| <div class="metric-value">{{ space.sdk }}</div> | |
| <div class="metric-label">SDK 版本</div> | |
| </div> | |
| <div class="metric-item"> | |
| <div class="metric-value">{{ '私有' if space.private else '公开' }}</div> | |
| <div class="metric-label">访问权限</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <a href="{{ space.url }}" target="_blank" class="action-button"> | |
| <i class="fas fa-external-link-alt"></i> | |
| 查看 | |
| </a> | |
| <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart"> | |
| <i class="fas fa-sync-alt"></i> | |
| 重启 | |
| </button> | |
| <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button"> | |
| <i class="fas fa-tools"></i> | |
| 重建 | |
| </button> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <div class="owner-section"> | |
| <p style="text-align: center; padding: 2rem; color: var(--text-secondary);"> | |
| <i class="fas fa-info-circle"></i> | |
| 没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。 | |
| </p> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> | |
| <script> | |
| const socket = io(); | |
| // 页面加载完成后隐藏加载动画 | |
| window.addEventListener('load', function() { | |
| document.getElementById('loading').style.display = 'none'; | |
| }); | |
| // 搜索功能 | |
| document.getElementById('spaceSearch').addEventListener('input', function(e) { | |
| const searchTerm = e.target.value.toLowerCase(); | |
| document.querySelectorAll('.space-card').forEach(card => { | |
| const spaceName = card.querySelector('.space-name').textContent.toLowerCase(); | |
| const spaceId = card.dataset.spaceId.toLowerCase(); | |
| if (spaceName.includes(searchTerm) || spaceId.includes(searchTerm)) { | |
| card.style.display = ''; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| }); | |
| // 主题切换 | |
| const themeToggle = document.querySelector('.theme-toggle'); | |
| themeToggle.addEventListener('click', function() { | |
| document.body.classList.toggle('light-theme'); | |
| const icon = this.querySelector('i'); | |
| icon.classList.toggle('fa-sun'); | |
| icon.classList.toggle('fa-moon'); | |
| }); | |
| // WebSocket 连接 | |
| socket.on('connect', () => { | |
| console.log('Connected to server'); | |
| }); | |
| socket.on('disconnect', () => { | |
| console.log('Disconnected from server'); | |
| }); | |
| socket.on('spaces_updated', (data) => { | |
| updateSpaceStatuses(); | |
| }); | |
| document.addEventListener('visibilitychange', function() { | |
| if (document.hidden) { | |
| socket.disconnect(); | |
| } else { | |
| socket.connect(); | |
| } | |
| }); | |
| function updateSpaceStatuses() { | |
| document.querySelectorAll('.space-card').forEach(card => { | |
| const spaceId = card.dataset.spaceId; | |
| fetch(`/api/space/${spaceId}/status`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| const statusElement = card.querySelector('.status-badge'); | |
| if (statusElement && data.status) { | |
| statusElement.className = `status-badge status-${data.status}`; | |
| statusElement.innerHTML = `<i class="fas fa-circle"></i> ${data.status}`; | |
| } | |
| }) | |
| .catch(error => console.error('Error updating status:', error)); | |
| }); | |
| } | |
| function confirmAction(action, spaceId) { | |
| const actionText = action === 'restart' ? '重启' : '重建'; | |
| if (confirm(`确定要${actionText} "${spaceId}" 吗?`)) { | |
| document.getElementById('loading').style.display = 'flex'; | |
| window.location.href = `/action/${action}/${spaceId}`; | |
| } | |
| } | |
| // 每30秒更新状态 | |
| setInterval(updateSpaceStatuses, 30000); | |
| // 添加卡片动画 | |
| document.querySelectorAll('.space-card').forEach(card => { | |
| card.addEventListener('mouseenter', function() { | |
| this.style.transform = 'translateY(-5px)'; | |
| }); | |
| card.addEventListener('mouseleave', function() { | |
| this.style.transform = 'translateY(0)'; | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |