249a1aaed9
- 后端: 添加 POST /api/retries/{id}/retry (手动重试) + DELETE /api/retries/{id}
+ 审计日志分页(offset/limit/count) + PushRetryJobRepo新增get_all/get_by_id/delete
- 前端retries: 手动重试按钮、删除按钮、状态过滤、操作人显示、toast反馈
- 前端audit: 分页控件(上一页/下一页)、每页50条、新增retry_job/delete_retry过滤
- 修复index.html: 适配审计API新返回格式(data.items)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
5.8 KiB
HTML
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="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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'';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
|
|
loadRetries();
|
|
</script>
|
|
</body></html>
|