Files
cms-SiteForge/app/static/operations.js
T
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:31:03 +08:00

47 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const $=s=>document.querySelector(s), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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=>`<div class="row"><div><b>${esc(j.kind||'build')} <span class="flag">${esc(j.state)}</span></b><small>${esc(j.id)} · 尝试 ${esc(j.attempts||0)} 次</small></div>${['queued','running','retrying'].includes(j.state)?`<button class="secondary" onclick="cancelJob('${esc(j.id)}')">取消</button>`:''}</div>`).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=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).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=>`<div class="row"><div><b>${esc(b.id)}</b><small>${esc(b.created_at)} · ${esc(b.total_pages)} 页</small></div><button class="secondary" onclick="requestApproval('${esc(b.id)}')">提交审批</button></div>`).join(''):'当前没有构建';
$('#releaseBuild').innerHTML=builds.map(b=>`<option value="${esc(b.id)}">${esc(b.id)} · ${esc(b.total_pages)} 页</option>`).join('');
$('#approvals').innerHTML=approvals.length?approvals.slice(0,12).map(a=>`<div class="row"><div><b>${esc(a.build_id)} <span class="flag ${a.state==='pending'?'warn':''}">${esc(a.state)}</span></b><small>由 ${esc(a.requested_by)} 提交</small></div>${a.state==='pending'?`<button class="secondary" onclick="approve('${esc(a.id)}')">批准</button>`:''}</div>`).join(''):'暂无审批记录';
$('#reviews').innerHTML=reviewData.items.length?reviewData.items.map(x=>`<div class="row"><div><b>${esc(x.platform)} · 第 ${esc(x.row_number)} 条</b><small>${esc(x.issue)}</small></div><button class="secondary" onclick="resolveReview('${esc(x.id)}')">已检查并处理</button></div>`).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=>`<div class="row"><div><b>${esc(x.build_id)} · ${esc(x.target)} · ${esc(x.release_id)}</b><small>${esc(x.deployed_at)}</small></div><button class="secondary" onclick="rollbackRelease('${esc(x.release_id)}','${esc(x.target)}')">回滚</button></div>`).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=`<b>输入 ${result.input} 条 · 可映射 ${result.mapped} 条 · 错误 ${result.errors.length} 条 · 复核项 ${result.warnings.length} 条</b><div>${result.errors.slice(0,12).map(x=>`<p>第${x.row}条:${esc(x.error)}</p>`).join('')}${result.warnings.slice(0,12).map(x=>`<p>待复核 第${x.row}条:${esc(x.issue)}</p>`).join('')}</div>`}}
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);