| {% extends "base.html" %} |
|
|
| {% block title %}部署状态 - HuggingFace Space 部署器{% endblock %} |
|
|
| {% block content %} |
| <div class="max-w-2xl mx-auto"> |
| |
| <div class="text-center mb-8"> |
| <h1 class="text-3xl font-bold mb-4"> |
| <i data-lucide="activity" class="w-8 h-8 inline mr-2"></i> |
| 部署状态监控 |
| </h1> |
| <p class="text-base-content/70">任务 ID: <code class="bg-base-300 px-2 py-1 rounded">{{ task_id }}</code></p> |
| </div> |
|
|
| |
| <div class="card bg-base-100 shadow-2xl border border-base-300 fade-in max-w-4xl mx-auto"> |
| <div class="card-body"> |
| <h2 class="card-title text-2xl mb-6 flex items-center"> |
| <i data-lucide="activity" class="w-6 h-6 mr-2"></i> |
| <span data-i18n="status.title">Deployment Status</span> |
| </h2> |
| |
| |
| <div class="bg-base-200 rounded-lg p-4 mb-6"> |
| <div class="flex items-center justify-between"> |
| <span class="text-sm font-semibold" data-i18n="status.taskId">Task ID</span> |
| <div class="flex items-center gap-2"> |
| <code class="text-xs bg-base-300 px-2 py-1 rounded" id="task-id">{{ task_id }}</code> |
| <button |
| onclick="copyToClipboard('{{ task_id }}')" |
| class="btn btn-ghost btn-xs" |
| data-i18n-title="status.copy" |
| title="Copy" |
| > |
| <i data-lucide="copy" class="w-3 h-3"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="status-container" class="min-h-[400px] relative transition-all duration-300"> |
| |
| <div class="status-content absolute inset-0 flex items-center justify-center"> |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="loading loading-spinner loading-lg text-primary mb-4"></div> |
| <h3 class="text-xl font-semibold mb-2" data-i18n="status.initializing">Initializing...</h3> |
| <p class="text-base-content/70" data-i18n="status.preparing">Preparing your Space...</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="refresh-indicator" class="mt-6 text-center transition-opacity duration-300"> |
| <p class="text-xs text-base-content/50"> |
| <i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1 animate-spin"></i> |
| <span data-i18n="status.autoRefresh">Auto-refresh every 2s</span> |
| </p> |
| </div> |
| |
| |
| <div class="divider mt-8"></div> |
| |
| <div class="flex gap-4 justify-center"> |
| <a href="/" class="btn btn-ghost"> |
| <i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i> |
| <span data-i18n="status.newDeploy">New Deploy</span> |
| </a> |
| <button |
| onclick="window.location.reload()" |
| class="btn btn-ghost" |
| > |
| <i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> |
| <span data-i18n="status.refresh">Refresh</span> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="alert alert-info mt-8"> |
| <i data-lucide="help-circle" class="w-5 h-5"></i> |
| <div> |
| <h3 class="font-bold">关于部署过程</h3> |
| <div class="text-sm mt-2"> |
| <p>• <strong>PENDING:</strong> 任务已创建,等待开始处理</p> |
| <p>• <strong>IN_PROGRESS:</strong> 正在克隆代码并部署到 HuggingFace Spaces</p> |
| <p>• <strong>SUCCESS:</strong> 部署成功,您的应用已上线</p> |
| <p>• <strong>FAILED:</strong> 部署失败,请检查错误信息</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <style> |
| |
| .status-content { |
| transition: opacity 0.3s ease-in-out; |
| } |
| |
| .status-content.fade-out { |
| opacity: 0; |
| } |
| |
| .status-content.fade-in { |
| opacity: 1; |
| } |
| |
| |
| #status-container { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| |
| .steps .step { |
| transition: all 0.3s ease; |
| } |
| </style> |
|
|
| <script> |
| // Deployment status monitoring |
| const taskId = '{{ task_id }}'; |
| let statusInterval = null; |
| let retryCount = 0; |
| const maxRetries = 3; |
| let currentStatus = null; |
| let lastUpdateTime = Date.now(); |
| |
| // Status templates |
| const statusTemplates = { |
| PENDING: () => ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="loading loading-dots loading-lg text-info mb-4"></div> |
| <h3 class="text-xl font-semibold mb-2">${t('status.queued')}</h3> |
| <p class="text-base-content/70">${t('status.queued.desc')}</p> |
| |
| <div class="mt-6"> |
| <span class="badge badge-info badge-lg"> |
| <i data-lucide="clock" class="w-4 h-4 mr-2"></i> |
| PENDING |
| </span> |
| </div> |
| </div> |
| `, |
| |
| IN_PROGRESS: () => ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="loading loading-spinner loading-lg text-primary mb-4"></div> |
| <h3 class="text-xl font-semibold mb-2">${t('status.progress')}</h3> |
| <p class="text-base-content/70">${t('status.progress.desc')}</p> |
| |
| <div class="mt-6"> |
| <span class="badge badge-primary badge-lg"> |
| <i data-lucide="loader-2" class="w-4 h-4 mr-2 loading-spinner"></i> |
| IN PROGRESS |
| </span> |
| </div> |
| |
| <div class="mt-8 w-full max-w-md"> |
| <ul class="steps steps-vertical lg:steps-horizontal w-full"> |
| <li class="step step-primary" data-step="1">Initialize</li> |
| <li class="step step-primary" data-step="2">Clone</li> |
| <li class="step step-primary" data-step="3">Build</li> |
| <li class="step" data-step="4">Deploy</li> |
| </ul> |
| </div> |
| </div> |
| `, |
| |
| SUCCESS: (detail) => ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="mb-4"> |
| <div class="w-20 h-20 bg-success/20 rounded-full flex items-center justify-center"> |
| <i data-lucide="check-circle" class="w-12 h-12 text-success"></i> |
| </div> |
| </div> |
| <h3 class="text-2xl font-bold mb-2 text-success">${t('status.success')}</h3> |
| <p class="text-base-content/70 mb-6">${t('status.success.desc')}</p> |
| |
| <div class="mt-4"> |
| <span class="badge badge-success badge-lg"> |
| <i data-lucide="check" class="w-4 h-4 mr-2"></i> |
| SUCCESS |
| </span> |
| </div> |
| |
| ${detail && detail.space_url ? ` |
| <div class="mt-8 p-6 bg-success/10 border border-success/20 rounded-lg w-full max-w-lg"> |
| <div class="text-center"> |
| <p class="text-sm text-base-content/70 mb-2">${t('status.url')}</p> |
| <div class="flex items-center justify-center gap-2 bg-base-100 p-3 rounded-lg"> |
| <a |
| href="${detail.space_url}" |
| target="_blank" |
| class="link link-primary text-lg font-mono truncate max-w-sm" |
| > |
| ${detail.space_url} |
| </a> |
| <button |
| onclick="copyToClipboard('${detail.space_url}')" |
| class="btn btn-ghost btn-sm" |
| title="${t('status.copy')}" |
| > |
| <i data-lucide="copy" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div class="mt-6 flex justify-center"> |
| <a |
| href="${detail.space_url}" |
| target="_blank" |
| class="btn btn-success" |
| > |
| <i data-lucide="external-link" class="w-4 h-4 mr-2"></i> |
| <span>${t('status.visit')}</span> |
| </a> |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| `, |
| |
| FAILED: (detail) => ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="mb-4"> |
| <div class="w-20 h-20 bg-error/20 rounded-full flex items-center justify-center"> |
| <i data-lucide="x-circle" class="w-12 h-12 text-error"></i> |
| </div> |
| </div> |
| <h3 class="text-2xl font-bold mb-2 text-error">${t('status.failed')}</h3> |
| <p class="text-base-content/70 mb-6">${t('status.failed.desc')}</p> |
| |
| <div class="mt-4"> |
| <span class="badge badge-error badge-lg"> |
| <i data-lucide="x" class="w-4 h-4 mr-2"></i> |
| FAILED |
| </span> |
| </div> |
| |
| ${detail && detail.error ? ` |
| <div class="mt-8 p-6 bg-error/10 border border-error/20 rounded-lg w-full max-w-lg"> |
| <div class="flex items-start gap-3"> |
| <i data-lucide="alert-triangle" class="w-5 h-5 text-error mt-0.5"></i> |
| <div class="flex-1"> |
| <p class="font-semibold text-error mb-2">${t('status.error')}</p> |
| <pre class="text-sm bg-base-100 p-3 rounded overflow-x-auto whitespace-pre-wrap">${detail.error}</pre> |
| </div> |
| </div> |
| |
| <div class="mt-6"> |
| <h4 class="font-semibold mb-2">${t('status.troubleshoot')}</h4> |
| <ul class="text-sm space-y-1 text-base-content/70"> |
| <li>• ${t('req.dockerfile')}</li> |
| <li>• ${t('req.token')}</li> |
| <li>• Verify repository URL is accessible</li> |
| <li>• Check Space name is unique</li> |
| </ul> |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| ` |
| }; |
| |
| // Smooth update function |
| function smoothUpdate(container, newContent) { |
| const currentContent = container.querySelector('.status-content'); |
| if (!currentContent) { |
| container.innerHTML = `<div class="status-content">${newContent}</div>`; |
| return; |
| } |
| |
| // Create new content element |
| const newElement = document.createElement('div'); |
| newElement.className = 'status-content absolute inset-0 flex items-center justify-center fade-out'; |
| newElement.innerHTML = newContent; |
| |
| // Add new content |
| container.appendChild(newElement); |
| |
| // Fade out old content and fade in new content |
| currentContent.classList.add('fade-out'); |
| |
| setTimeout(() => { |
| newElement.classList.remove('fade-out'); |
| newElement.classList.add('fade-in'); |
| |
| setTimeout(() => { |
| currentContent.remove(); |
| newElement.classList.remove('absolute'); |
| }, 300); |
| }, 50); |
| } |
| |
| // Fetch status from API |
| async function fetchStatus() { |
| try { |
| const response = await fetch(`/deploy/status/${taskId}`); |
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| |
| // Only update if status changed or enough time has passed |
| if (data.status !== currentStatus || Date.now() - lastUpdateTime > 10000) { |
| updateStatus(data); |
| currentStatus = data.status; |
| lastUpdateTime = Date.now(); |
| } |
| |
| retryCount = 0; // Reset retry count on successful fetch |
| |
| } catch (error) { |
| console.error('Failed to fetch status:', error); |
| retryCount++; |
| |
| if (retryCount >= maxRetries) { |
| stopStatusPolling(); |
| showError('Failed to fetch deployment status. Please refresh the page.'); |
| } |
| } |
| } |
| |
| // Update status display |
| function updateStatus(data) { |
| const container = document.getElementById('status-container'); |
| const template = statusTemplates[data.status]; |
| |
| if (template) { |
| smoothUpdate(container, template(data.detail)); |
| |
| // Re-initialize icons after transition |
| setTimeout(() => { |
| if (typeof lucide !== 'undefined') { |
| lucide.createIcons(); |
| } |
| |
| // Update translations |
| if (typeof updatePageTranslations !== 'undefined') { |
| updatePageTranslations(); |
| } |
| }, 350); |
| |
| // Stop polling if final state |
| if (data.status === 'SUCCESS' || data.status === 'FAILED') { |
| stopStatusPolling(); |
| |
| // Show toast notification |
| if (data.status === 'SUCCESS') { |
| showToast(t('toast.deploySuccess'), 'success'); |
| } else { |
| showToast(t('toast.deployFailed'), 'error'); |
| } |
| } |
| } |
| } |
| |
| // Show error message |
| function showError(message) { |
| const container = document.getElementById('status-container'); |
| const errorContent = ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="mb-4"> |
| <div class="w-20 h-20 bg-error/20 rounded-full flex items-center justify-center"> |
| <i data-lucide="alert-triangle" class="w-12 h-12 text-error"></i> |
| </div> |
| </div> |
| <h3 class="text-xl font-bold mb-2 text-error">Connection Error</h3> |
| <p class="text-base-content/70">${message}</p> |
| </div> |
| `; |
| |
| smoothUpdate(container, errorContent); |
| |
| setTimeout(() => { |
| if (typeof lucide !== 'undefined') { |
| lucide.createIcons(); |
| } |
| }, 350); |
| } |
| |
| // Start status polling |
| function startStatusPolling() { |
| // Initial fetch |
| fetchStatus(); |
| |
| // Set up interval |
| statusInterval = setInterval(fetchStatus, 2000); |
| } |
| |
| // Stop status polling |
| function stopStatusPolling() { |
| if (statusInterval) { |
| clearInterval(statusInterval); |
| statusInterval = null; |
| } |
| |
| // Fade out refresh indicator |
| const indicator = document.getElementById('refresh-indicator'); |
| if (indicator) { |
| indicator.style.opacity = '0'; |
| setTimeout(() => { |
| indicator.style.display = 'none'; |
| }, 300); |
| } |
| } |
| |
| // Initialize on page load |
| document.addEventListener('DOMContentLoaded', function() { |
| startStatusPolling(); |
| |
| // Re-initialize icons |
| setTimeout(() => { |
| if (typeof lucide !== 'undefined') { |
| lucide.createIcons(); |
| } |
| }, 100); |
| }); |
| |
| // Clean up on page unload |
| window.addEventListener('beforeunload', function() { |
| stopStatusPolling(); |
| }); |
| </script> |
| {% endblock %} |