Spaces:
Paused
Paused
| <html lang="zh"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HF Space Manager -控制面板</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| } | |
| body { | |
| background-color: #f5f5f7; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .header { | |
| background: rgba(255, 255, 255, 0.85); | |
| backdrop-filter: blur(20px); | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);z-index: 1000; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| color: #1d1d1f; | |
| font-weight: 600; | |
| } | |
| .logout { | |
| background: none; | |
| border: none; | |
| color: #0071e3; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| transition: all 0.3s ease;text-decoration: none; | |
| } | |
| .logout:hover { | |
| background: rgba(0, 113, 227, 0.1); | |
| } | |
| .container { | |
| flex: 1; | |
| width: 100%; | |
| max-width: 1200px; | |
| margin: 100px auto 0; | |
| padding: 0 2rem 2rem; | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(255, 255, 255, 0.8); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 9999; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 5px solid #f3f3f3; | |
| border-top: 5px solid #0071e3; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .owner-section { | |
| margin-bottom: 2rem;background: white; | |
| border-radius: 18px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); | |
| } | |
| .owner-name { | |
| font-size: 1.3rem; | |
| font-weight: 600; | |
| color: #1d1d1f; | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 1px solid #e5e5e7; | |
| display: flex; | |
| justify-content: space-between;align-items: center; | |
| } | |
| .space-count { | |
| font-size: 0.9rem; | |
| color: #86868b;} | |
| .space-status-count { | |
| margin-left: 10px; | |
| } | |
| .space-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 1.5rem; | |
| width: 100%; | |
| } | |
| .space-card { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | |
| transition: all 0.3s ease; | |
| display: flex; | |
| flex-direction: column;} | |
| .space-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |
| } | |
| .space-name { | |
| font-size: 1.1rem; | |
| font-weight: 500; | |
| color: #1d1d1f; | |
| margin-bottom: 1rem; | |
| } | |
| .space-info { | |
| font-size: 0.9rem; | |
| color: #86868b; | |
| margin-bottom: 1rem; | |
| flex-grow: 1; | |
| } | |
| .space-info p { | |
| margin-bottom: 0.5rem; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: 500;} | |
| .status-BUILDING { | |
| background-color: #ff9500; | |
| color: white; | |
| } | |
| .status-RUNNING { | |
| background-color: #34c759; | |
| color: white; | |
| } | |
| .status-SLEEPING { | |
| background-color: #007aff; | |
| color: white; | |
| } | |
| .status-STOPPED { | |
| background-color: #8e8e93; | |
| color: white; | |
| } | |
| .status-FAILED { | |
| background-color: #ff3b30; | |
| color: white; | |
| } | |
| .status-BUILD_ERROR { | |
| background-color: #ff3b30; | |
| color: white; | |
| } | |
| .status-UNKNOWN { | |
| background-color: #8e8e93; | |
| color: white; | |
| } | |
| .action-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 0.5rem; | |
| margin-top: auto; | |
| } | |
| .action-button { | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| border: none; | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease;text-align: center; | |
| text-decoration: none; | |
| } | |
| .view { | |
| background: #e5e5e7; | |
| color: #1d1d1f; | |
| } | |
| .view:hover { | |
| background: #d5d5d7; | |
| } | |
| .restart { | |
| background: #0071e3; | |
| color: white; | |
| } | |
| .restart:hover { | |
| background: #0077ED; | |
| } | |
| .rebuild { | |
| background: #f5f5f7; | |
| color: #1d1d1f; | |
| } | |
| .rebuild:hover { | |
| background: #e5e5e7; | |
| } | |
| .footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: #86868b; | |
| font-size: 0.9rem; | |
| background: white; | |
| border-top: 1px solid #e5e5e7; | |
| } | |
| .footer a { | |
| color: #0071e3; | |
| text-decoration: none;transition: color 0.3s ease; | |
| } | |
| .footer a:hover { | |
| color: #0077ED; | |
| text-decoration: underline; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 0 1rem; | |
| } | |
| .owner-name { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .space-count { | |
| margin-top: 0.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading" class="loading-overlay"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| <header class="header"> | |
| <h1>HF Space Manager</h1> | |
| <a href="/logout" class="logout">退出登录</a> | |
| </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 %} | |
| {% for owner, owner_spaces in grouped_spaces.items() %} | |
| {% set sorted_spaces = owner_spaces %} | |
| {% set running_count = sorted_spaces | selectattr('status','equalto','RUNNING') | list | length %} | |
| {% set building_count = sorted_spaces | selectattr('status','equalto','BUILDING') | list | length %} | |
| {% set sleeping_count = sorted_spaces | selectattr('status','equalto','SLEEPING') | list | length %} | |
| {% set stopped_count = sorted_spaces | selectattr('status','equalto','STOPPED') | list | length %} | |
| {% set failed_count = sorted_spaces | selectattr('status','equalto','BUILD_ERROR') | list | length %} | |
| <div class="owner-section"> | |
| <div class="owner-name"> | |
| <span>{{ owner }}</span> | |
| <span class="space-count"> | |
| 总数: {{ sorted_spaces | length }} | |
| <span class="space-status-count">运行:{{ running_count }}</span> | |
| <span class="space-status-count">休眠:{{ sleeping_count }}</span> | |
| <span class="space-status-count">停止:{{ stopped_count }}</span> | |
| <span class="space-status-count">失败:{{ failed_count }}</span> | |
| </span> | |
| </div><div class="space-grid"> | |
| {% for space in sorted_spaces %} | |
| <div class="space-card" data-space-id="{{ space.repo_id }}"><div class="space-name">{{ space.name }}</div> | |
| <div class="space-info"> | |
| <p>ID: {{ space.repo_id }}</p> | |
| <p>状态: <span class="status-badge status-{{ space.status }}">{{ space.status }}</span></p><p>创建时间: {{ space.created_at }}</p> | |
| <p>最后修改: {{ space.last_modified }}</p> | |
| <p>SDK: {{ space.sdk }}</p><p>App端口: {{ space.app_port }}</p> | |
| {% if space.tags %} | |
| <p>标签:{% for tag in space.tags %}<span class="tag">{{ tag }}</span> | |
| {% endfor %} | |
| </p> | |
| {% endif %}<p>私有: {{ '是' if space.private else '否' }}</p> | |
| </div><div class="action-buttons"> | |
| <a href="{{ space.url }}" target="_blank" class="action-button view">查看</a> | |
| <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">重启</button> | |
| <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button rebuild">重建</button> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <div class="owner-section"> | |
| <p style="text-align: center; color: #86868b;">没有找到任何 Spaces。请确保你的账户中有创建的Spaces,并且提供的 token 有正确的权限。</p> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <footer class="footer"> | |
| <a href="https://github.com/ssfun/hf-space-manager">HF Space Manager</a> 由 | |
| <a href="https://github.com/ssfun">ssfun</a> 构建,源代码遵循 | |
| <a href="https://opensource.org/license/mit">MIT 协议</a> | |
| </footer> | |
| <script> | |
| // 页面加载完成后隐藏加载动画 | |
| window.addEventListener('load', function() { | |
| document.getElementById('loading').style.display = 'none'; | |
| }); | |
| // 定期更新状态 | |
| 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.textContent = data.status; | |
| } | |
| }) | |
| .catch(error => console.error('Error updating status:', error)); | |
| }); | |
| } | |
| // 在进行操作时显示加载动画 | |
| function confirmAction(action, spaceId) { | |
| var actionText = action === 'restart' ? '重启' : '重建'; | |
| if (confirm(`确定要${actionText} "${spaceId}" 吗?`)) {document.getElementById('loading').style.display = 'flex'; | |
| window.location.href = `/action/${action}/${spaceId}`; | |
| } | |
| } | |
| // 每60秒更新一次状态 | |
| setInterval(updateSpaceStatuses, 60000); | |
| </script> | |
| </body> | |
| </html> | |