Files
Nexus/web/app/retries.html
T
Your Name 69056491a7 Fix fmtTime Invalid Date for timezone-aware timestamps (+00:00)
fmtTime() was appending 'Z' to all timestamps, causing Invalid Date
when the timestamp already had a timezone suffix like +00:00.
Now tries new Date(t) first, falls back to new Date(t+'Z').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:34:51 +08:00

83 lines
5.8 KiB
HTML

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 重试队列</title><script src="/app/vendor/tailwindcss-browser.js"></script><script defer src="/app/vendor/alpinejs.min.js"></script><style type="text/tailwindcss">@theme{--color-brand:oklch(55% 0.2 250);--color-brand-light:oklch(75% 0.15 250);--color-brand-dark:oklch(35% 0.18 250)}</style></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0" data-sidebar></aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">重试队列</h1></div>
<div class="flex items-center gap-3">
<select id="statusFilter" onchange="loadRetries()" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
<option value="">全部状态</option>
<option value="pending">等待中</option>
<option value="running">执行中</option>
<option value="failed">失败</option>
<option value="success">成功</option>
</select>
<button onclick="loadRetries()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-white text-sm rounded-lg transition">刷新</button>
</div>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="retryList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
</main>
</div>
<script src="/app/api.js"></script>
<script src="/app/layout.js"></script>
<script>
initLayout('retries');
let _retriesCache=[];
async function loadRetries(){
try{
const r=await apiFetch(API+'/retries/');if(!r)return;
let jobs=await r.json();
_retriesCache=jobs;
// Client-side status filter
const statusFilter=document.getElementById('statusFilter').value;
if(statusFilter)jobs=jobs.filter(j=>j.status===statusFilter);
document.getElementById('retryList').innerHTML=jobs.length?jobs.map(j=>{
const statusColor=j.status==='pending'?'text-yellow-400':j.status==='running'?'text-blue-400':j.status==='failed'?'text-red-400':'text-green-400';
const statusIcon=j.status==='pending'?'⏳':j.status==='running'?'🔄':j.status==='failed'?'❌':'✅';
const retryPct=Math.min((j.retry_count/j.max_retries)*100,100);
const canRetry=j.status==='failed'||j.status==='pending';
return `<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-sm">${statusIcon}</span>
<span class="text-white font-medium">${esc(j.server_name||'#'+j.server_id)}</span>
<span class="${statusColor} text-xs px-2 py-0.5 rounded ${j.status==='failed'?'bg-red-400/10':j.status==='pending'?'bg-yellow-400/10':j.status==='success'?'bg-green-400/10':'bg-blue-400/10'}">${esc(j.status)}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-400">重试 ${j.retry_count}/${j.max_retries}</span>
${canRetry?`<button onclick="retryJob(${j.id})" class="text-brand-light text-xs hover:underline px-2 py-1 bg-brand/10 rounded">重试</button>`:''}
<button onclick="deleteJob(${j.id})" class="text-red-400 text-xs hover:underline px-2 py-1 bg-red-400/10 rounded">删除</button>
</div>
</div>
<div class="mt-2 text-xs text-slate-500">${esc(j.source_path)}${esc(j.target_path||'')}</div>
${j.operator?`<div class="mt-1 text-xs text-slate-600">操作人: ${esc(j.operator)}</div>`:''}
${j.last_error?`<div class="mt-1 text-xs text-red-400/70 truncate" title="${esc(j.last_error)}">${esc(j.last_error.substring(0,100))}</div>`:''}
<div class="mt-2 w-full bg-slate-800 rounded-full h-1"><div class="h-1 rounded-full ${retryPct>=80?'bg-red-400':retryPct>=50?'bg-yellow-400':'bg-brand'}" style="width:${retryPct}%"></div></div>
${j.next_retry_at?`<div class="mt-1 text-xs text-slate-600">下次重试: ${fmtTime(j.next_retry_at)}</div>`:''}
</div>`}).join(''):'<div class="text-slate-500 text-center py-8">暂无重试任务</div>';
}catch(e){document.getElementById('retryList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
}
async function retryJob(id){
const r=await apiFetch(API+'/retries/'+id+'/retry',{method:'POST'});
if(r&&r.ok)toast('success','重试任务已触发');else toast('error','触发重试失败');
loadRetries();
}
async function deleteJob(id){
if(!confirm('确定删除此重试任务?'))return;
const r=await apiFetch(API+'/retries/'+id,{method:'DELETE'});
if(r&&r.ok)toast('success','重试任务已删除');else toast('error','删除失败');
loadRetries();
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmtTime(t){if(!t)return'';var d=new Date(t);if(isNaN(d.getTime())){d=new Date(t+'Z')}return d.toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
loadRetries();
</script>
</body></html>