'use strict';
/* SiteForge simplified workspace: native APIs, no parallel business backend. */
const $=(s,root=document)=>root.querySelector(s);
const esc=x=>String(x??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
const themeName={automotive:'汽车与出行',industrial:'工业与制造',corporate:'企业品牌',minimal:'极简展示','trade-pro':'Trade Pro'};
const typeName={page:'页面',product:'产品',vehicle:'车型',post:'新闻',brand:'品牌',case:'案例',service:'服务'};
const fmtDate=t=>{if(!t)return '—';const d=new Date(t);return Number.isNaN(d.getTime())?String(t).slice(0,16):d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'})};
const sfSiteKey='siteforge:selected-site';
const sfRemember=id=>{try{if(id)localStorage.setItem(sfSiteKey,id)}catch{}};
const state={sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null,selectionToken:0,building:false};
const headings={home:'工作台',sites:'我的网站',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'构建与交付',settings:'网站设置'};
async function api(url,method='GET',data=null,extraHeaders={}){
const headers={...extraHeaders};if(method!=='GET'&&method!=='HEAD')headers['X-SiteForge-CSRF']=state.csrf;
let body=data;if(data!=null && !(data instanceof FormData)){headers['Content-Type']='application/json';body=JSON.stringify(data)}
const res=await fetch(url,{method,headers,body,credentials:'same-origin'});
if(res.status===401){location.href='/login';throw Error('登录已过期,请重新登录')}
let value;try{value=await res.json()}catch{throw Error('服务器响应异常(HTTP '+res.status+')')}
if(!res.ok){const ex=Error(typeof value.detail==='string'?value.detail:JSON.stringify(value.detail||value));ex.status=res.status;throw ex}return value;
}
function toast(msg,bad=false){const el=$('#toast');el.textContent=msg;el.classList.toggle('bad',bad);el.classList.add('show');clearTimeout(el.timer);el.timer=setTimeout(()=>el.classList.remove('show'),4000)}
function notifyError(error){console.error(error);toast(error?.message||String(error),true)}
function htmlHeading(title,desc,actions=''){return `
SITEFORGE / ${esc(headings[state.view]||'WORKSPACE')}
${esc(title)} ${esc(desc)}
${actions}
`}
function actionButton(text,action=''){return `${esc(text)} `}
function stat(title,num,icon){return `${icon} ${title} ${esc(num)}
`}
function emptyState(title,description,button=''){return `◇
${esc(title)} ${esc(description)}
${button}
`}
function nav(button){return `data-action="navigate" data-view="${button}"`}
// Business navigation is a shareable, browser-history-aware URL; selection is still authorized by /api/sites.
const siteUrl=(view,site)=>{const q=new URLSearchParams();if(site)q.set('site',site);if(view&&(view!=='home'||site))q.set('view',view);return '/'+(q.size?'?'+q.toString():'')};
function rememberView(replace=false){const url=siteUrl(state.view,state.site?.id);history[replace?'replaceState':'pushState']({site:state.site?.id||null,view:state.view},'',url)}
function activeJobs(){return state.jobs.filter(j=>['queued','running','retrying'].includes(j.state))}
function buildStatusLabel(stateCode){return ({queued:'等待 Worker',running:'正在生成',retrying:'重试中',done:'已完成',failed:'失败',cancelled:'已取消'})[stateCode]||stateCode||'未知'}
function buildReadyStep(){return state.entries.some(e=>e.status!=='draft'&&e.type!=='page')||state.entries.some(e=>e.status!=='draft'&&e.type==='page')}
function siteCard(s){return `${esc((s.name||'S').slice(0,1))} ${esc(s.name)} ${esc(s.domain||'尚未设置正式域名')}
${esc(themeName[s.theme]||'企业网站')} · ${Number(s.entries_count)||0} 条内容 · ${Number(s.build_count)||0} 次构建 › `}
function overviewShortcut(view,icon,title,desc){return `${icon} ${title} ${desc}
前往操作 → `}
function home(){const sites=state.sites;const total=sites.reduce((n,s)=>n+(+s.entries_count||0),0);const builds=sites.reduce((n,s)=>n+(+s.build_count||0),0);
return htmlHeading('欢迎回来','管理和制作企业网站,从这里开始。',actionButton('+ 创建新网站','new-site'))+
`${stat('网站项目',sites.length,'▤')}${stat('内容总数',total,'▧')}${stat('构建版本',builds,'↗')}${stat('可用主题',4,'◈')}
`+
`您想先做什么? 按使用目的进入对应页面,不需要了解技术术语。
`+
overviewShortcut('sites','+','创建或管理网站','从零搭建网站,或者接着制作已有网站。')+
overviewShortcut('import','⇩','迁移旧站 / 导入数据','导入 WordPress 或 Excel 产品资料。')+
overviewShortcut('design','◈','更换网站设计','选择行业主题,编辑网页与全站外观。')+
`
`+
(sites.length?`${sites.slice(0,4).map(siteCard).join('')}
`:emptyState('还没有网站项目','创建一个网站,选择主题后即可开始添加页面、产品和新闻。',actionButton('+ 创建第一个网站','new-site')));
}
function sites(){return htmlHeading('我的网站','统一管理所有网站项目,每个网站的数据、主题和构建记录彼此独立。',actionButton('+ 创建网站','new-site'))+
(state.sites.length?`${state.sites.map(siteCard).join('')}
`:emptyState('暂时没有网站','选择行业主题,开始制作您的第一个网站。',actionButton('创建网站','new-site')));
}
function currentSite(){return !!state.site}
function noSite(){return htmlHeading('请先选择一个网站','选择现有网站或创建新网站,才能继续进行内容编辑、迁移和构建。')+emptyState('尚未选择网站','点击右上角选择网站,或直接创建一个新的企业网站。',actionButton('+ 创建网站','new-site'))}
function latestDone(){return state.builds.find(b=>['done','complete','success','completed'].includes(b.status))||null}
function overview(){const s=state.site;if(!s)return noSite();const count=state.entries.length;const build=latestDone();const hasContent=buildReadyStep();const hasDomain=!!s.domain;const steps=[
{title:'建立网站',desc:'选择行业主题并创建项目',done:true,view:'settings'},
{title:'准备内容',desc:'添加页面、产品或导入旧数据',done:hasContent,view:'content'},
{title:'调整设计',desc:'可反复完善主题、导航和移动端样式',optional:true,view:'design'},
{title:'构建并检查',desc:'生成静态网站,查看质量报告',done:!!build,view:'build'}];
const next=!hasContent?steps[1]:!build?steps[3]:null;
return htmlHeading(s.name,'这里显示真实的内容和构建状态。设计可随时调整,正式上线需要单独审核。',actionButton('↗ 构建网站','build-now'))+
`${next?'建议下一步:'+next.title:'网站已经生成过构建版本'} ${next?next.desc:'内容或设计有修改时,重新构建并检查新版即可。'}。${!hasDomain?'正式域名还未填写;测试预览不受影响。':''}
${next?'现在去做':'查看构建版本'} → `+
`${stat('网站内容',count,'▧')}${stat('已构建版本',state.builds.length,'↗')}${stat('当前主题',themeName[s.theme]||'企业品牌','◈')}${stat('域名状态',hasDomain?'已填写':'待设置','◎')}
`+
`四步完成网站制作 步骤反映已有数据,设计可以随时修改,不会虚报“设计完成”。
`+
`${steps.map((step,i)=>`
${step.done?'✓':step.optional?'◈':i+1} ${step.title}${step.optional?'(持续优化)':''} ${step.desc}
打开 → `).join('')}
`+
``+
`${overviewShortcut('content','▧','添加和编辑内容','管理页面、产品、新闻和案例。')}${overviewShortcut('design','◈','修改网站样式','更换主题、编辑页面和移动端样式。')}${overviewShortcut('build','↗','查看和下载网站','查看任务状态、预览及下载静态网站。')}
`;
}
function content(){if(!currentSite())return noSite();let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.search||`${e.title} ${e.route}`.toLowerCase().includes(state.search.toLowerCase())));
return htmlHeading('内容与页面','所有网页、产品、车型和新闻都在这里维护,保存后重新构建即可更新网站。',actionButton('+ 添加内容','new-entry'))+
`内容列表 共 ${state.entries.length} 条内容。点击“设计页面”可进行可视化排版。
全部类型 ${Object.entries(typeName).map(([key,value])=>`${value} `).join('')}
`+
(arr.length?`
名称 内容类型 页面地址 状态 操作 ${arr.slice(0,200).map(e=>`${esc(e.title)} ${esc(typeName[e.type]||e.type)} ${esc(e.route)} ${e.status==='draft'?'草稿':'已保存'} 修改内容 设计页面 ↗ `).join('')}
`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+
`${arr.length>200?'
当前显示前 200 条,请使用搜索缩小范围。
':''}
`;
}
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`${name}
${name} ${s.theme===id?'当前使用 ':''} ${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象','trade-pro':'适合外贸 B2B 英文站点与询盘'})[id]}
${s.theme===id?'正在使用':'使用此主题'} `).join('');
return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+
`${cards}
`+
`第二步 · 设计网页 挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。
选择要设计的页面 修改先保存为草稿,正式发布设计后才会参与下一次静态构建。
提示 :网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。
`;
}
function imports(){if(!currentSite())return noSite();return htmlHeading('导入与迁移','根据您已有的资料选择操作方式:Excel 产品、WordPress 网站,或者外部 API / 数据库。')+
``+
`推荐:先预演再导入 在数据导入工作台检查字段匹配、页面路径冲突及品牌分类关系,再分批提交。
打开安全导入工作台 → 高级:直接写入网站(不可批次回滚) 仅用于确认内容无误的小文件,建议先备份;复杂迁移请使用上方工作台。
`+
`迁移旧网站请注意: 如果需要保留旧 URL、原有 SEO、复杂插件字段和全部图片,请优先使用 WordPress 迁移工作台,而不是仅上传一个内容文件。
`;
}
function builds(){if(!currentSite())return noSite();const latest=latestDone();const pending=activeJobs();const first=pending[0];
return htmlHeading('构建与交付','网站构建由独立 Worker 执行。离开页面或刷新后仍可查看任务状态。',`${pending.length?'已有构建在运行':'↗ 开始构建'} `)+
`${first?'当前:'+buildStatusLabel(first.state):latest?'已有构建版本,可以预览':'还没有已完成的网站构建'} ${first?'当前任务 '+esc(first.id.slice(0,12))+';Worker 在后台执行,您可以关闭此页面。':'内容或设计有更新时,重新生成网站版本。'}
`+
`后台任务 可以查看任务状态。仅取消当前仍在队列或运行中的构建。
刷新状态 ${esc(state.job||'')}
`+
(state.jobs.length?state.jobs.slice(0,8).map(j=>`
${j.state==='done'?'✓':j.state==='failed'?'!':'◷'} ${['queued','running','retrying'].includes(j.state)?`
取消任务 `:''}
`).join(''):'
当前网站还没有后台构建任务。
')+`
`+
`构建版本 建议先预览和核对质量报告,再下载 ZIP。预览不代表正式发布。
`+
(state.builds.length?state.builds.slice(0,20).map(b=>`
${['done','completed','success','complete'].includes(b.status)?'✓':'◷'} ${['done','completed','success','complete'].includes(b.status)?`
预览 ↗ 质量报告 下载 ZIP `:'
尚未完成,暂不能下载 '}
`).join(''):emptyState('尚无完成版本','准备好网站数据后,在上方提交构建任务。'))+
`
`;
}
function settings(){if(!currentSite())return noSite();const s=state.site;return htmlHeading('网站设置','这里只管理公司资料、域名与站点属性。网页样式请到“外观与设计”修改。')+
`基本资料 没有域名也可以制作与预览网站,正式上线前再填写即可。
`;
}
function rerender(){const area=$('#mainContent');const selected=state.view;$('#breadcrumbTitle').textContent=headings[selected]||'工作台';$('#sideSiteName').textContent=state.site?.name||'请先选择网站';
document.querySelectorAll('[data-view].nav-item').forEach(el=>{const yes=el.dataset.view===selected;el.classList.toggle('active',yes);yes?el.setAttribute('aria-current','page'):el.removeAttribute('aria-current')});
area.innerHTML=({home,sites,overview,content,design,import:imports,build:builds,settings}[selected]||home)();
}
async function loadSites(){state.sites=await api('/api/sites');const picker=$('#sitePicker'),prev=state.site?.id||'';picker.innerHTML='选择网站… '+state.sites.map(s=>`${esc(s.name)} `).join('');picker.value=state.sites.some(s=>s.id===prev)?prev:''}
async function readJobs(siteId){try{return await api(`/api/enterprise/sites/${encodeURIComponent(siteId)}/jobs`)}catch(e){if(![403,404].includes(Number(e.status)))console.warn('加载任务状态失败',e);return []}}
async function selectSite(id,go='overview',historyMode='push'){
const token=++state.selectionToken;
if(!id){state.site=null;state.entries=[];state.builds=[];state.jobs=[];state.job=null;state.view=go==='home'?'home':'sites';$('#sitePicker').value='';rerender();if(historyMode!=='none')rememberView(historyMode==='replace');return}
if(!state.sites.some(x=>x.id===id)){toast('该网站不在您的可见列表中',true);return}
try{const [site,entries,builds,jobs]=await Promise.all([api(`/api/sites/${encodeURIComponent(id)}`),api(`/api/sites/${encodeURIComponent(id)}/entries`),api(`/api/sites/${encodeURIComponent(id)}/builds`),readJobs(id)]);
if(token!==state.selectionToken)return;
state.site=site;sfRemember(id);state.entries=entries;state.builds=builds;state.jobs=jobs;state.job=null;state.view=go;state.search='';state.typ='';$('#sitePicker').value=id;rerender();if(historyMode!=='none')rememberView(historyMode==='replace');closeMobile();window.scrollTo({top:0,behavior:'instant'});
}catch(e){if(token===state.selectionToken)notifyError(e)}
}
function navigate(to,historyMode='push'){if(!state.site&&!['home','sites'].includes(to)){state.view='sites';rerender();toast('请先选择网站项目');return}state.view=to;rerender();closeMobile();if(historyMode!=='none')rememberView(historyMode==='replace');if(to==='build')refreshBuilds();window.scrollTo({top:0,behavior:'instant'})}
async function refreshBuilds(){const id=state.site?.id;if(!id)return;try{const previouslyRunning=activeJobs().length>0;const [jobs,builds]=await Promise.all([readJobs(id),api(`/api/sites/${encodeURIComponent(id)}/builds`)]);if(state.site?.id!==id)return;state.jobs=jobs;state.builds=builds;if(previouslyRunning&&!activeJobs().length){const last=jobs[0];state.job=last?.state==='done'?'后台构建已完成,请预览并查看质量报告。':last?.state==='failed'?'本次构建失败,请在任务中心检查原因。':last?.state==='cancelled'?'本次构建已取消。':null}if(state.view==='build')rerender()}catch(e){notifyError(e)}}
function openModal(name){const node=$('#'+name);node.showModal();node.querySelector('input:not([type=hidden]),select')?.focus()}
function closeModal(name){$('#'+name)?.close()}
function openEntry(id){const form=$('#entryForm');form.reset();form.elements.entry_id.value='';$('#entryModalTitle').textContent=id?'修改内容':'添加内容';if(id){const e=state.entries.find(x=>x.id===id);if(!e)return toast('这条内容不存在',true);for(const field of ['title','route','summary','body','seo_title','seo_description','type','status']){if(form.elements[field])form.elements[field].value=e[field]||''}form.elements.entry_id.value=id}openModal('entryModal')}
async function saveEntry(ev){ev.preventDefault();if(!state.site)return;const el=ev.currentTarget;const data=Object.fromEntries(new FormData(el));const id=data.entry_id;delete data.entry_id;
if(!data.route?.trim())delete data.route;
try{await api(`/api/sites/${state.site.id}/entries${id?'/'+encodeURIComponent(id):''}`,id?'PATCH':'POST',data);closeModal('entryModal');toast(id?'内容已更新':'内容已添加');await selectSite(state.site.id,'content')}catch(e){notifyError(e)}}
async function saveSite(ev){ev.preventDefault();try{const f=ev.currentTarget;const data=Object.fromEntries(new FormData(f));const s=await api('/api/sites','POST',data);closeModal('siteModal');toast('网站创建成功,现在可以添加内容');await loadSites();await selectSite(s.id,'overview')}catch(e){notifyError(e)}}
async function setTheme(theme){if(!state.site||theme===state.site.theme)return;if(!confirm(`切换到「${themeName[theme]}」?网站内容及已有 URL 将保持不变,外观会在下次构建后更新。`))return;try{await api(`/api/sites/${state.site.id}`,'PATCH',{theme});toast('主题已切换,重新构建后生效');await selectSite(state.site.id,'design')}catch(e){notifyError(e)}}
async function saveSettings(ev){ev.preventDefault();try{await api(`/api/sites/${state.site.id}`,'PATCH',Object.fromEntries(new FormData(ev.currentTarget)));toast('网站设置已保存');await loadSites();await selectSite(state.site.id,'settings')}catch(e){notifyError(e)}}
async function importFile(ev){ev.preventDefault();try{if(!state.site)return;const form=new FormData(ev.currentTarget);if(!form.get('file')?.name)throw Error('请选择文件');if(!confirm('将文件内容直接写入当前网站,重复路径默认跳过。需要预演和批次回滚请先使用高级导入。确认继续?'))return;const result=await api(`/api/sites/${state.site.id}/import`,'POST',form);const box=$('#importSummary');box.hidden=false;box.textContent=`导入完成:新增 ${result.imported||0} 条,更新 ${result.updated||0} 条,跳过 ${result.skipped||0} 条。错误 ${result.errors?.length||0} 条。`;toast('导入已完成');await refreshSiteKeepView()}catch(e){notifyError(e)}}
async function refreshSiteKeepView(){if(!state.site)return;const [site,entries,builds]=await Promise.all([api('/api/sites/'+state.site.id),api('/api/sites/'+state.site.id+'/entries'),api('/api/sites/'+state.site.id+'/builds')]);state.site=site;state.entries=entries;state.builds=builds}
async function cancelJob(jobId){if(!state.site)return;if(!confirm('确定取消这个构建任务?已经完成的静态版本不会被删除。'))return;try{const id=state.site.id;await api(`/api/enterprise/sites/${encodeURIComponent(id)}/jobs/${encodeURIComponent(jobId)}/cancel`,'POST');toast('已发送取消请求');await refreshBuilds()}catch(e){notifyError(e)}}
async function buildNow(){if(!state.site)return toast('请先选择网站',true);if(state.building||activeJobs().length)return toast('已有构建在执行,请先查看任务状态');
const id=state.site.id;state.building=true;
try{navigate('build');state.job='正在提交构建任务…';rerender();const job=await api(`/api/sites/${encodeURIComponent(id)}/builds`,'POST',{mirror_images:false},{'Idempotency-Key':crypto.randomUUID()});
if(state.site?.id!==id)return;state.job=`任务 ${job.id?.slice(0,12)||'已提交'} 已进入队列,可以离开页面;重新打开“构建与交付”查看结果。`;toast('已提交构建任务');await refreshBuilds();
}catch(e){notifyError(e)}finally{state.building=false;if(state.site?.id===id&&state.view==='build')rerender()}}
async function checkReadiness(){if(!state.site)return;const siteId=state.site.id;
try{const result=await api(`/api/factory/sites/${encodeURIComponent(siteId)}/migration/readiness`);
if(state.site?.id!==siteId)return;
const box=$('#reportContent');if(!box)return;
const gates=result.gates||[];
box.innerHTML=`正式交付前检查 检查旧站 URL、SEO 和静态文件风险;通过检查不等于已经上线。
${result.can_publish?'没有发现发布阻断项':'存在需要先处理的问题'}
${gates.length?'
'+gates.map(g=>`${g.level==='block'?'必须修复':'需要复核'} ${esc(g.message)} `).join('')+' ':'
当前没有需要处理的检查项。仍需人工确认内容、域名与询盘表单。
'}
`;
box.scrollIntoView({behavior:'smooth',block:'nearest'});
}catch(e){notifyError(e)}
}
async function showReport(buildId){try{const r=await api(`/api/sites/${state.site.id}/builds/${buildId}/report`);const target=$('#reportContent');if(!target)return;target.innerHTML=`质量检查结果 本报告检查静态内容与资源,不保证搜索引擎排名。
${stat('HTML 页面',r.pages||0,'▤')}${stat('重定向',r.redirects||0,'⇄')}${stat('已本地化图片',r.mirrored_images||0,'▧')}${stat('提示数量',(r.warnings||[]).length,'!')}
${r.warnings?.length?`
${r.warnings.slice(0,20).map(x=>esc(x)).join('\n')}
`:'
当前检查没有警告 '}
`;target.scrollIntoView({behavior:'smooth',block:'nearest'})}catch(e){notifyError(e)}}
function closeMobile(){$('#sideMenu').classList.remove('open');$('#mobileBackdrop').classList.remove('open');$('#menuToggle').setAttribute('aria-expanded','false')}
async function init(){try{const me=await api('/api/auth/me');state.csrf=me.csrf;await loadSites();const health=await api('/api/health');$('#appVersion').textContent='版本 '+health.version;
// Restore URL first, then a previously selected site *only* after verifying /api/sites.
const params=new URLSearchParams(location.search),requested=params.get('site');
const target=params.get('view');const allowed=new Set(['home','sites','overview','content','design','import','build','settings']);
let stored='';try{stored=localStorage.getItem(sfSiteKey)||''}catch{}
const chosen=[requested,stored].find(id=>id&&state.sites.some(x=>x.id===id));
if(chosen){await selectSite(chosen,allowed.has(target)?target:'overview','replace')}
else {state.view=target==='sites'?'sites':'home';rerender();rememberView(true)}
}catch(e){notifyError(e);$('#mainContent').innerHTML=emptyState('后台加载失败',e.message,'重新加载 ')}}
// Delegated UI interactions keep navigation and form actions centrally auditable.
document.addEventListener('click',async ev=>{const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='new-site')openModal('siteModal');if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id)}catch(e){notifyError(e)}});
document.addEventListener('click',ev=>{const btn=ev.target.closest('[data-close]');if(btn)closeModal(btn.dataset.close)});
$('#siteForm').addEventListener('submit',saveSite);$('#entryForm').addEventListener('submit',saveEntry);
document.addEventListener('submit',ev=>{if(ev.target.id==='settingsForm')saveSettings(ev);if(ev.target.id==='importForm')importFile(ev)});
document.addEventListener('input',ev=>{if(ev.target.id==='contentSearch'){state.search=ev.target.value;const cursor=ev.target.selectionStart;const type=state.typ;rerender();const el=$('#contentSearch');el.focus();el.setSelectionRange(cursor,cursor);state.typ=type}});
document.addEventListener('change',ev=>{if(ev.target.id==='contentType'){state.typ=ev.target.value;rerender()}});
$('#sitePicker').addEventListener('change',ev=>selectSite(ev.target.value));
window.addEventListener('popstate',async()=>{const p=new URLSearchParams(location.search);const site=p.get('site'),view=p.get('view')||'home';const allowed=['home','sites','overview','content','design','import','build','settings'];if(site&&state.sites.some(s=>s.id===site))await selectSite(site,allowed.includes(view)?view:'overview','none');else{state.selectionToken++;state.site=null;state.entries=[];state.jobs=[];state.builds=[];state.view=view==='sites'?'sites':'home';rerender()}});
setInterval(()=>{if(!document.hidden&&state.view==='build'&&activeJobs().length)refreshBuilds()},5000);
$('#newSiteTop').addEventListener('click',()=>openModal('siteModal'));
$('#logoutBtn').addEventListener('click',async()=>{try{await api('/api/auth/logout','POST');location.href='/login'}catch(e){notifyError(e)}});
$('#menuToggle').addEventListener('click',()=>{const open=$('#sideMenu').classList.toggle('open');$('#mobileBackdrop').classList.toggle('open',open);$('#menuToggle').setAttribute('aria-expanded',String(open))});
$('#mobileBackdrop').addEventListener('click',closeMobile);
document.addEventListener('keydown',ev=>{if(ev.key==='Escape')closeMobile()});
document.addEventListener('DOMContentLoaded',init);