'use strict'; const $=s=>document.querySelector(s), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let csrf='',sites=[],siteId='',jobs=[],builds=[],releases=[],lastPreview=null; const notify=(message,error=false)=>{$('#notice').textContent=message;$('#notice').style.color=error?'#ac3d45':'#256448'}; async function request(url,{method='GET',body=null,upload=false}={}){ const headers={};if(method!=='GET')headers['X-SiteForge-CSRF']=csrf; if(body&&!upload)headers['Content-Type']='application/json'; const res=await fetch(url,{method,credentials:'same-origin',headers,body:body?(upload?body:JSON.stringify(body)):undefined}); if(res.status===401){location.assign('/login');throw Error('登录已过期')} const result=await res.json();if(!res.ok)throw Error(result.detail||'请求失败');return result; } const post=(url,body)=>request(url,{method:'POST',body}); function formatJobs(list){return list.length?list.map(j=>`
${esc(j.kind||'build')} ${esc(j.state)}${esc(j.id)} · 尝试 ${esc(j.attempts||0)} 次
${['queued','running','retrying'].includes(j.state)?``:''}
`).join(''):'当前没有任务';} async function chooseSite(){siteId=$('#site').value;window.SiteForgeShell?.selectSite(siteId);lastPreview=null;$('#commitImport').disabled=true;await reload()} async function init(){try{csrf=(await request('/api/auth/me')).csrf;sites=await request('/api/sites');$('#site').innerHTML=sites.map(s=>``).join('');siteId=sites[0]?.id||'';$('#site').onchange=chooseSite;$('#reload').onclick=reload;$('#previewImport').onclick=()=>importJSON(false);$('#commitImport').onclick=()=>importJSON(true);$('#build').onclick=buildSite;$('#deploy').onclick=deploySite;$('#refreshRelease').onclick=reload;await reload()}catch(e){notify(e.message,true)}} async function reload(){if(!siteId)return;try{ const [list,jobsData,buildData,approvals,history,reviewData,deliveryData]=await Promise.all([ request(`/api/sites/${siteId}`),request(`/api/enterprise/sites/${siteId}/jobs`),request(`/api/sites/${siteId}/builds`),request(`/api/factory/sites/${siteId}/release-requests`),request(`/api/enterprise/sites/${siteId}/releases`),request(`/api/migrations/sites/${siteId}/reviews`),request(`/api/production/sites/${siteId}/delivery-status`)]); jobs=jobsData;builds=buildData;releases=history;$('#statBuild').textContent=builds.length;$('#statJobs').textContent=jobs.filter(j=>['queued','running','retrying'].includes(j.state)).length;$('#statApprovals').textContent=approvals.filter(j=>j.state==='pending').length;$('#statMode').textContent=list.mode==='migration'?'迁移':'新建';$('#jobList').innerHTML=formatJobs(jobs); $('#buildList').innerHTML=builds.length?builds.slice(0,14).map(b=>`
${esc(b.id)}${esc(b.created_at)} · ${esc(b.total_pages)} 页
`).join(''):'当前没有构建'; $('#releaseBuild').innerHTML=builds.map(b=>``).join(''); $('#approvals').innerHTML=approvals.length?approvals.slice(0,12).map(a=>`
${esc(a.build_id)} ${esc(a.state)}由 ${esc(a.requested_by)} 提交
${a.state==='pending'?``:''}
`).join(''):'暂无审批记录'; $('#reviews').innerHTML=reviewData.items.length?reviewData.items.map(x=>`
${esc(x.platform)} · 第 ${esc(x.row_number)} 条${esc(x.issue)}
`).join(''):'暂无待复核事项'; $('#releaseBackend').textContent=['staging','production'].map(t=>`${t}: ${deliveryData.backends?.[t]||'agent'} · ${deliveryData.configured?.[t]?'已配置':'未配置'}`).join(' | '); $('#releases').innerHTML=deliveryData.history?.length?deliveryData.history.slice(0,10).map(x=>`
${esc(x.build_id)} · ${esc(x.target)} · ${esc(x.release_id)}${esc(x.deployed_at)}
`).join(''): '暂无远程发布记录'; const entries=await request(`/api/sites/${siteId}/entries`);const entry=entries.find(p=>p.route==='/')||entries[0];$('#edit').href=entry?`/editor/${siteId}/${entry.id}`:'/factory-studio'; }catch(e){notify(e.message,true)}} async function importJSON(commit){const file=$('#file').files?.[0];if(!file)return notify('请选择 JSON 文件',true);if(!siteId)return;const platform=$('#platform').value; if(commit&&!lastPreview)return;const data=new FormData();data.append('platform',platform);data.append('file',file);if(commit)data.append('conflict','skip'); try{const result=await request(`/api/migrations/sites/${siteId}/${commit?'import':'preview'}`,{method:'POST',body:data,upload:true});if(commit){notify(`导入完成:${result.result.imported} 条,人工复核 ${result.migration_warnings.length} 项`);lastPreview=null;$('#commitImport').disabled=true;await reload()}else{lastPreview=result;$('#commitImport').disabled=result.errors.length>0;$('#report').innerHTML=`输入 ${result.input} 条 · 可映射 ${result.mapped} 条 · 错误 ${result.errors.length} 条 · 复核项 ${result.warnings.length} 条
${result.errors.slice(0,12).map(x=>`

第${x.row}条:${esc(x.error)}

`).join('')}${result.warnings.slice(0,12).map(x=>`

待复核 第${x.row}条:${esc(x.issue)}

`).join('')}
`}} catch(e){notify(e.message,true)}} async function resolveReview(id){ const answer=prompt('请详细填写对照旧站完成检查/修复的说明(至少12个字符)。未经核查不要批准。'); if(answer===null)return; try{await post(`/api/migrations/sites/${siteId}/reviews/${id}/resolve`,{resolution:answer});notify('复核记录已保存');await reload()}catch(e){notify(e.message,true)} } async function buildSite(){try{const task=await post(`/api/sites/${siteId}/builds`,{mirror_images:false});notify(`任务 ${task.id} 已进入 Worker 队列`);await reload()}catch(e){notify(e.message,true)}} async function cancelJob(id){try{await post(`/api/enterprise/sites/${siteId}/jobs/${id}/cancel`,{});await reload()}catch(e){notify(e.message,true)}} async function requestApproval(build){try{await post(`/api/factory/sites/${siteId}/release-requests`,{build_id:build});notify('已提交发布审批');await reload()}catch(e){notify(e.message,true)}} async function approve(id){if(!confirm('审批人必须与申请人不同。确定审批通过吗?'))return;try{await post(`/api/factory/sites/${siteId}/release-requests/${id}/review`,{state:'approved'});notify('审批已完成');await reload()}catch(e){notify(e.message,true)}} async function deploySite(){if(!confirm('系统将使用目标环境预先配置的发布适配器。请确认构建已审批、DNS 与版本回退方案已准备好。继续发布?'))return;try{const data=await post(`/api/production/sites/${siteId}/deploy`,{build_id:$('#releaseBuild').value,target:$('#target').value});notify(`发布任务 ${data.id} 已提交,Worker 将先上传、校验,再切换线上版本。`);await reload()}catch(e){notify(e.message,true)}} async function rollbackRelease(release,target){if(!confirm(`确认将 ${target} 恢复为历史版本 ${release}?`))return; try{const result=await post(`/api/production/sites/${siteId}/rollback`,{release_id:release,target});notify(`回滚任务 ${result.id} 已进入队列`);await reload()}catch(e){notify(e.message,true)}} window.rollbackRelease=rollbackRelease; window.resolveReview=resolveReview;window.cancelJob=cancelJob;window.requestApproval=requestApproval;window.approve=approve; window.addEventListener('DOMContentLoaded',init);