|
|
{% 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 %} |