wasrusgen1-crm/docs/crm.html

303 lines
26 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM консультанта · @wasrusgen1 | КОНСАЛТИНГ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--primary:#047857;--dark:#064E3B;--mid:#10B981;--light:#ECFDF5;--ink:#0F0F1A;--bg:#F5F6F8;--white:#fff;--border:#E5E7EB;--text:#1A1A2E;--muted:#6B7280}
html,body{height:100%;overflow:hidden}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
.hdr{height:54px;background:var(--dark);display:flex;align-items:center;padding:0 20px;gap:12px;flex-shrink:0;z-index:10}
.hdr-ic{width:30px;height:30px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;color:#fff;font-size:16px}
.hdr-t{font-family:'Montserrat';font-weight:700;font-size:14px;color:rgba(255,255,255,.6);display:flex;align-items:center;gap:9px;letter-spacing:-.2px}
.hdr-sep{width:1.5px;height:15px;background:rgba(255,255,255,.25);flex-shrink:0}
.hdr-t b{font-weight:800;color:#fff}
.hdr-badge{background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.25);color:var(--mid);font-size:10px;font-weight:700;letter-spacing:.05em;border-radius:6px;padding:2px 8px}
.hdr-r{margin-left:auto;display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.6);font-size:13px}
.layout{flex:1;display:flex;overflow:hidden}
/* Sidebar */
.sb{width:280px;flex-shrink:0;background:var(--ink);display:flex;flex-direction:column;overflow:hidden}
.sb-top{padding:16px 16px 10px}
.sb-newbtn{width:100%;display:flex;align-items:center;justify-content:center;gap:7px;padding:11px;border-radius:10px;background:var(--primary);color:#fff;border:none;cursor:pointer;font-family:'Inter';font-weight:700;font-size:13px}
.sb-newbtn:hover{background:#065f46}
.sb-cap{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.25);padding:14px 18px 6px}
.sb-list{flex:1;overflow-y:auto;padding:0 10px}
.proj{padding:12px 12px;border-radius:10px;cursor:pointer;margin-bottom:4px;border-left:2px solid transparent;transition:background .12s}
.proj:hover{background:rgba(255,255,255,.04)}
.proj.active{background:rgba(4,120,87,.18);border-left-color:var(--primary)}
.proj-top{display:flex;align-items:center;gap:8px;margin-bottom:5px}
.proj-av{width:30px;height:30px;border-radius:8px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#fff;flex-shrink:0}
.proj-name{font-size:13px;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.proj-niche{font-size:11px;color:rgba(255,255,255,.4);margin-bottom:6px;padding-left:38px}
.proj-flags{display:flex;gap:4px;padding-left:38px}
.flag{font-size:9px;font-weight:700;padding:2px 6px;border-radius:5px;background:rgba(255,255,255,.06);color:rgba(255,255,255,.3)}
.flag.on{background:rgba(16,185,129,.18);color:var(--mid)}
.sb-foot{padding:12px 14px;border-top:1px solid rgba(255,255,255,.07)}
.sb-elena{display:flex;align-items:center;gap:9px;padding:8px;border-radius:9px;background:rgba(4,120,87,.1);border:1px solid rgba(4,120,87,.2)}
.sb-elena-av{width:30px;height:30px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;font-size:13px;color:#fff}
.sb-elena-n{font-size:12px;font-weight:600;color:#fff}
.sb-elena-s{font-size:10px;color:var(--mid)}
/* Main */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.empty-main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#cbd5e1;gap:12px}
.topbar{height:60px;background:var(--white);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 26px;gap:14px;flex-shrink:0}
.tb-av{width:38px;height:38px;border-radius:10px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:15px}
.tb-name{font-size:16px;font-weight:700;color:var(--text)}
.tb-meta{font-size:12px;color:var(--muted);margin-top:1px}
.tb-status{margin-left:auto;font-size:11px;font-weight:700;padding:5px 12px;border-radius:20px;background:var(--light);color:var(--primary)}
/* Tabs */
.tabs{display:flex;gap:2px;background:var(--white);border-bottom:1px solid var(--border);padding:0 20px;flex-shrink:0}
.tab{padding:13px 18px;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2.5px solid transparent;display:flex;align-items:center;gap:7px;white-space:nowrap}
.tab:hover{color:var(--text)}
.tab.active{color:var(--primary);border-bottom-color:var(--primary)}
.tab-check{width:15px;height:15px;border-radius:50%;background:var(--mid);display:none;align-items:center;justify-content:center;font-size:9px;color:#fff}
.tab.done .tab-check{display:flex}
/* Content */
.content{flex:1;overflow-y:auto;padding:24px 26px;background:var(--bg)}
.pane{display:none}.pane.active{display:block}
.run-card{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:28px;text-align:center;max-width:560px;margin:20px auto}
.run-ic{font-size:36px;margin-bottom:14px}
.run-t{font-family:'Montserrat';font-weight:800;font-size:20px;color:var(--ink);margin-bottom:8px}
.run-d{font-size:14px;color:var(--muted);line-height:1.5;margin-bottom:20px}
.run-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 24px;border-radius:11px;background:linear-gradient(135deg,var(--dark),var(--primary));color:#fff;border:none;cursor:pointer;font-family:'Inter';font-weight:700;font-size:14px}
.run-btn:hover{opacity:.92}.run-btn:disabled{opacity:.5;cursor:default}
/* Checkpoint bar */
.cpbar{position:sticky;bottom:0;background:var(--white);border:1.5px solid var(--border);border-radius:13px;padding:14px 18px;display:flex;align-items:center;gap:14px;margin-top:20px;box-shadow:0 -4px 16px rgba(0,0,0,.04)}
.cpbar.approved{background:#F0FDF4;border-color:rgba(16,185,129,.3)}
.cpbar-txt{flex:1;font-size:13px;color:var(--text);font-weight:600}
.cpbar-sub{font-size:11px;color:var(--muted);font-weight:400;margin-top:1px}
.cp-btn{padding:9px 18px;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;border:none;font-family:'Inter'}
.cp-approve{background:var(--primary);color:#fff}.cp-approve:hover{background:var(--dark)}
.cp-redo{background:transparent;border:1px solid var(--border);color:var(--muted)}
.cp-done{color:var(--primary);font-weight:700;font-size:13px;display:flex;align-items:center;gap:6px}
/* generic blocks (reused) */
.blk{background:var(--white);border:1px solid var(--border);border-radius:13px;padding:16px;margin-bottom:12px}
.blk-title{font-size:14px;font-weight:700;color:var(--text)}
.blk-pain{font-size:12px;color:#92400e;line-height:1.45;padding-left:14px;position:relative;margin-bottom:3px}
.blk-pain::before{content:'•';position:absolute;left:3px;color:#f59e0b}
.blk-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin-bottom:3px}
/* Canvas grid */
.canvas-grid{display:grid;grid-template-columns:repeat(5,1fr);grid-auto-rows:minmax(90px,auto);gap:8px}
.cv{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:11px}
.cv-h{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--primary);margin-bottom:6px;display:flex;justify-content:space-between}
.cv-pct{color:#9ca3af}
.cv li{font-size:11.5px;color:#374151;line-height:1.35;list-style:none;padding-left:10px;position:relative;margin-bottom:3px}
.cv li::before{content:'';position:absolute;left:2px;top:7px;width:4px;height:4px;border-radius:50%;background:var(--mid)}
.cv-note{font-size:10px;color:#92400e;margin-top:6px;font-style:italic}
.cv.kp{grid-row:span 2}.cv.ka{}.cv.vp{grid-column:3;grid-row:span 2;border-color:var(--primary);border-width:1.5px}.cv.cr{}.cv.cs{grid-column:5;grid-row:span 2}
.cv-insight{grid-column:1/-1;background:var(--ink);color:#fff;border-radius:11px;padding:14px 18px;font-size:13px;line-height:1.5}
.cv-insight b{color:var(--mid)}
/* IDEF0 (reused compact) */
.idef-lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin:14px 0 8px}
.idef{margin-bottom:12px}
.idef-c,.idef-m{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;padding:4px 0}
.idef-c{border-bottom:2px dashed #cbd5e1}.idef-m{border-top:2px dashed #cbd5e1}
.idef-mid{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:stretch}
.idef-i,.idef-o{display:flex;flex-direction:column;gap:4px;justify-content:center;max-width:140px}
.idef-fn{background:var(--white);border:2px solid var(--primary);border-radius:10px;padding:11px 9px;text-align:center;font-size:12.5px;font-weight:700;color:var(--dark);display:flex;flex-direction:column;gap:3px;justify-content:center}
.idef-fn b{font-size:10px;color:var(--primary);font-family:'Montserrat'}
.idef-fn i{font-style:normal;font-size:10px;font-weight:700}
.ar{font-size:10px;line-height:1.3;padding:3px 7px;border-radius:6px;background:#F1F5F9;color:#475569}
.ar em{font-style:normal;color:#94a3b8;font-size:9px;margin-left:2px}
.idef-c .ar{background:#FEF3C7;color:#92400E}.idef-c .ar.nomiss{background:#FEF2F2;color:#DC2626;border:1px dashed #FECACA}
.idef-i .ar{background:#EFF6FF;color:#1E40AF}.idef-o .ar{background:#ECFDF5;color:#047857}.idef-o .ar.dead{background:#FEF2F2;color:#DC2626}
.idef-m .ar{background:#F5F3FF;color:#6D28D9}
/* Spec */
.spec-sec{margin-bottom:24px}
.spec-h{font-family:'Montserrat';font-weight:800;font-size:16px;color:var(--ink);margin-bottom:12px;display:flex;align-items:center;gap:8px}
.spec-h .pl{width:24px;height:24px;border-radius:7px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800}
.mod{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:16px;margin-bottom:10px}
.mod-top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.mod-name{font-size:14px;font-weight:700;color:var(--text)}
.mod-node{font-size:10px;font-weight:700;color:var(--primary);background:var(--light);padding:2px 7px;border-radius:5px}
.mod-screens{display:flex;flex-wrap:wrap;gap:5px;margin:8px 0}
.scr{font-size:11px;background:#EFF6FF;color:#1E40AF;border:1px solid #BFDBFE;border-radius:6px;padding:3px 9px}
.mod-data{font-size:12px;color:#374151;line-height:1.4;margin-top:4px}
.ent{background:var(--white);border:1px solid var(--border);border-radius:11px;padding:14px;margin-bottom:8px}
.ent-name{font-size:13px;font-weight:700;color:var(--text);margin-bottom:8px;font-family:'Montserrat'}
.ent-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:5px;margin-bottom:8px}
.fld{font-size:11px;background:var(--bg);border-radius:6px;padding:4px 8px}
.fld b{color:var(--text)}.fld em{font-style:normal;color:#6366F1;font-size:10px}
.ent-ex{font-size:10.5px;color:#6b7280;font-family:monospace;background:var(--bg);border-radius:6px;padding:6px 9px;line-height:1.4}
.chat-mini{max-width:760px}
.cm{display:flex;gap:9px;margin-bottom:12px;align-items:flex-end}
.cm.out{flex-direction:row-reverse}
.cm-av{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;color:#fff}
.cm-av.e{background:var(--primary)}.cm-av.u{background:#6366F1}
.cm-b{padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.5;white-space:pre-wrap;max-width:78%}
.cm-b.in{background:var(--white);border:1px solid var(--border);border-bottom-left-radius:4px}
.cm-b.out{background:var(--primary);color:#fff;border-bottom-right-radius:4px}
.spin{display:inline-block;animation:sp 1s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-ic">@</div>
<div class="hdr-t">wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div>
<div class="hdr-badge">CRM КОНСУЛЬТАНТА</div>
<div class="hdr-r"><span style="width:8px;height:8px;border-radius:50%;background:var(--mid);display:inline-block"></span>Руслан</div>
</header>
<div class="layout">
<aside class="sb">
<div class="sb-top"><button class="sb-newbtn" onclick="newProject()">+ Новый проект</button></div>
<div class="sb-cap">Проекты клиентов</div>
<div class="sb-list" id="projList"></div>
<div class="sb-foot"><div class="sb-elena"><div class="sb-elena-av">Е</div><div><div class="sb-elena-n">Елена · Opus 4.8</div><div class="sb-elena-s">движок анализа</div></div></div></div>
</aside>
<main class="main" id="main">
<div class="empty-main"><div style="font-size:40px">📋</div><div>Выберите проект слева<br>или создайте новый</div></div>
</main>
</div>
<script>
const API="https://claude-83-172-150-111.sslip.io/elena";
let projects=[], current=null, state=null, activeTab="interview";
function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
async function loadProjects(){
const r=await fetch(`${API}/api/projects`); const d=await r.json();
projects=d.projects;
document.getElementById("projList").innerHTML = projects.map(p=>`
<div class="proj ${p.token===current?'active':''}" onclick="openProject('${p.token}')">
<div class="proj-top"><div class="proj-av">${esc((p.client_name||'?')[0])}</div><div class="proj-name">${esc(p.client_name)}</div></div>
<div class="proj-niche">${esc(p.niche)} · ${p.msg_count} сообщ</div>
<div class="proj-flags"><span class="flag ${p.has_canvas?'on':''}">Canvas</span><span class="flag ${p.has_idef0?'on':''}">IDEF0</span><span class="flag ${p.has_spec?'on':''}">ТЗ</span></div>
</div>`).join("");
}
async function newProject(){
const name=prompt("Имя клиента:"); if(name===null)return;
const niche=prompt("Ниша / направление:")||"";
const r=await fetch(`${API}/api/project/new`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({client_name:name,niche})});
const d=await r.json(); await loadProjects(); openProject(d.token);
}
async function openProject(token){
current=token;
const r=await fetch(`${API}/api/project/${token}`); state=await r.json();
await loadProjects(); renderMain();
}
const TABS=[
{id:"interview",name:"Интервью",icon:"💬"},
{id:"methods",name:"Методологии",icon:"🎯"},
{id:"canvas",name:"Стратегия",icon:"📊"},
{id:"idef0",name:"Функции",icon:"🔧"},
{id:"spec",name:"ТЗ",icon:"📋"}
];
function approved(stage){return localStorage.getItem(`appr_${current}_${stage}`)==="1"}
function approve(stage){localStorage.setItem(`appr_${current}_${stage}`,"1");renderMain()}
function unapprove(stage){localStorage.removeItem(`appr_${current}_${stage}`);renderMain()}
function renderMain(){
const p=projects.find(x=>x.token===current);
document.getElementById("main").innerHTML=`
<div class="topbar">
<div class="tb-av">${esc((state.client_name||'?')[0])}</div>
<div><div class="tb-name">${esc(state.client_name||'Без имени')}</div><div class="tb-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div>
<div class="tb-status">${esc(statusLabel(state.status))}</div>
</div>
<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}<span class="tab-check">✓</span></div>`).join("")}</div>
<div class="content" id="content"></div>`;
renderTab();
}
function statusLabel(s){return {interview:"Интервью",model_ready:"Модель готова",spec_ready:"ТЗ готово"}[s]||s}
function setTab(t){activeTab=t;renderMain()}
function cpBar(stage,label){
if(approved(stage)) return `<div class="cpbar approved"><div class="cpbar-txt">${label}<div class="cpbar-sub">Утверждено вами</div></div><div class="cp-done">✓ Утверждено</div><button class="cp-btn cp-redo" onclick="unapprove('${stage}')">Снять</button></div>`;
return `<div class="cpbar"><div class="cpbar-txt">✋ Контрольная точка<div class="cpbar-sub">${label}</div></div><button class="cp-btn cp-redo" onclick="rerun('${stage}')">Перестроить</button><button class="cp-btn cp-approve" onclick="approve('${stage}')">Утвердить →</button></div>`;
}
function runCard(stage,icon,title,desc,btnLabel){
return `<div class="run-card"><div class="run-ic">${icon}</div><div class="run-t">${title}</div><div class="run-d">${desc}</div><button class="run-btn" id="runbtn-${stage}" onclick="rerun('${stage}')">${btnLabel}</button></div>`;
}
function renderTab(){
const c=document.getElementById("content");
if(activeTab==="interview"){
c.innerHTML=`<div class="chat-mini">${state.messages.map(m=>`<div class="cm ${m.role==='user'?'out':''}"><div class="cm-av ${m.role==='user'?'u':'e'}">${m.role==='user'?'К':'Е'}</div><div class="cm-b ${m.role==='user'?'out':'in'}">${fmt(m.content)}</div></div>`).join("")||'<div style="color:#9ca3af;padding:40px;text-align:center">Интервью ещё не начато. Клиент общается с Еленой в своём кабинете.</div>'}</div>`;
}
else if(activeTab==="methods"){
if(!state.selection){c.innerHTML=runCard("methods","🎯","Подбор методологий","Елена проанализирует бизнес клиента и предложит набор методологий моделирования под его тип.","Подобрать методологии →");return}
const s=state.selection;
c.innerHTML=`<div class="blk"><div class="blk-lbl">Тип бизнеса</div><div style="font-size:16px;font-weight:700;margin-bottom:14px">${esc(s.business_type)}</div>
${s.recommended.map(r=>`<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--bg)"><span style="font-size:18px">${r.use?'✅':'⬜'}</span><div style="flex:1"><b>${r.method.toUpperCase()}</b> <span style="font-size:11px;color:#9ca3af">[${r.depth}]</span><div style="font-size:12px;color:var(--muted);margin-top:2px">${esc(r.reason)}</div></div></div>`).join("")}
<div style="margin-top:14px;padding:12px;background:var(--light);border-radius:9px;font-size:13px;color:var(--dark)">${esc(s.rationale)}</div></div>
${cpBar("methods","Согласны с набором методологий?")}`;
}
else if(activeTab==="canvas"){
if(!state.canvas){c.innerHTML=runCard("canvas","📊","Business Model Canvas","Стратегический разрез бизнеса — 9 блоков: ценность, клиенты, каналы, доходы, издержки.","Построить Canvas →");return}
c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегическая модель верна?");
}
else if(activeTab==="idef0"){
if(!state.model){c.innerHTML=runCard("idef0","🔧","Функциональная модель IDEF0","Разбор бизнеса на функции с входами, выходами, нормами и механизмами. Анализ разрывов.","Построить IDEF0 →");return}
c.innerHTML=renderIdef(state.model)+cpBar("idef0","Функциональная модель верна?");
}
else if(activeTab==="spec"){
if(!state.spec){
if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">⚠️</div><div class="run-t">Сначала постройте IDEF0</div><div class="run-d">ТЗ собирается из функциональной модели. Перейдите на вкладку «Функции» и постройте IDEF0.</div></div>`;return}
c.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует ПО: роли, модули, экраны, модель данных. Готово для разработчика.","Собрать ТЗ →");return
}
c.innerHTML=renderSpec(state.spec)+cpBar("spec","ТЗ готово к передаче разработчику?");
}
}
function renderCanvas(c){
const B=(k,cls,lbl)=>{const b=c[k];return `<div class="cv ${cls}"><div class="cv-h">${lbl}<span class="cv-pct">${b.completeness}%</span></div><ul>${b.items.map(i=>`<li>${esc(i)}</li>`).join("")}</ul>${b.note?`<div class="cv-note">${esc(b.note)}</div>`:''}</div>`};
return `<div class="canvas-grid">
${B("key_partners","kp","Партнёры")}${B("key_activities","ka","Активности")}${B("value_propositions","vp","Ценность")}${B("customer_relationships","cr","Отношения")}${B("customer_segments","cs","Сегменты")}
${B("key_resources","kr","Ресурсы")}${B("channels","ch","Каналы")}
${B("cost_structure","co","Издержки")}${B("revenue_streams","rev","Доходы")}
<div class="cv-insight"><b>Вывод:</b> ${esc(c.insight)}</div>
</div>`;
}
function renderIdef(m){
const box=(fn,ctrls,ins,outs,mechs,id,pct)=>{
const C=(ctrls&&ctrls.length)?ctrls.map(c=>`<span class="ar">${esc(c.name)}</span>`).join(""):`<span class="ar nomiss">нет управления</span>`;
const I=(ins||[]).map(a=>`<span class="ar">${esc(a.name)}</span>`).join("")||'<span class="ar">—</span>';
const O=(outs||[]).map(a=>`<span class="ar ${a.target==='НИКУДА'?'dead':''}">${esc(a.name)}${a.target==='НИКУДА'?' ⊘':''}</span>`).join("")||'<span class="ar">—</span>';
const M=(mechs||[]).map(x=>`<span class="ar">${esc(x.name)}</span>`).join("")||'<span class="ar">—</span>';
return `<div class="idef"><div class="idef-c">${C}</div><div class="idef-mid"><div class="idef-i">${I}</div><div class="idef-fn">${id?`<b>${id}</b>`:''}${esc(fn)}${pct!=null?`<i style="color:${pct>=70?'#047857':pct>=45?'#F59E0B':'#EF4444'}">${pct}%</i>`:''}</div><div class="idef-o">${O}</div></div><div class="idef-m">${M}</div></div>`;
};
let h=`<div style="background:var(--ink);color:#fff;border-radius:11px;padding:12px 16px;margin-bottom:14px;font-size:13px"><b style="color:var(--mid)">Паттерн:</b> ${esc(m.business_pattern)}</div>`;
if(m.context){h+=`<div class="idef-lbl">A-0 Контекст</div>`+box(m.context.function,m.context.controls,m.context.inputs,m.context.outputs,m.context.mechanisms,"A0");}
h+=`<div class="idef-lbl">Декомпозиция · ${m.activities.length} функций</div>`;
m.activities.forEach(a=>{h+=box(a.function,a.controls,a.inputs,a.outputs,a.mechanisms,a.node_id,a.completeness)});
if(m.arrow_issues&&m.arrow_issues.length){h+=`<div class="idef-lbl">Разрывы · ${m.arrow_issues.length}</div>`;m.arrow_issues.forEach(g=>{const col=g.severity==='critical'?'#DC2626':g.severity==='high'?'#92400E':'#1E40AF';h+=`<div class="blk" style="border-left:3px solid ${col};padding:11px 14px"><div style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)} · ${g.type}</div><div class="blk-title" style="font-size:13px">${esc(g.title)}</div><div style="font-size:12px;color:#6b7280;margin-top:3px">${esc(g.description)}</div></div>`})}
return h;
}
function renderSpec(s){
let h=`<div class="spec-sec"><div class="spec-h"><span class="pl">A</span>Обзор системы</div><div class="blk">${esc(s.overview)}</div></div>`;
h+=`<div class="spec-sec"><div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;
s.roles.forEach(r=>h+=`<div class="blk" style="padding:12px 16px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);
h+=`</div><div class="spec-sec"><div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;
s.modules.forEach(m=>{h+=`<div class="mod"><div class="mod-top"><span class="mod-node">${esc(m.source_node)}</span><span class="mod-name">${esc(m.name)}</span></div><div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(m.purpose)}</div><div class="mod-screens">${m.screens.map(sc=>`<span class="scr">${esc(sc)}</span>`).join("")}</div><div class="mod-data">📥 ${esc(m.inputs_data)} · 📤 ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:6px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}</div>`});
h+=`</div><div class="spec-sec"><div class="spec-h"><span class="pl">C</span>Модель данных (${s.entities.length} таблиц)</div>`;
s.entities.forEach(e=>{h+=`<div class="ent"><div class="ent-name">◆ ${esc(e.name)}</div><div class="ent-fields">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:6px">🔗 ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`});
h+=`</div>`;
if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div class="blk-title" style="margin-bottom:8px">Уточнить перед разработкой</div>${s.open_questions.map(q=>`<div class="blk-pain">${esc(q)}</div>`).join("")}</div>`}
return h;
}
const RUN={methods:["select-methodologies","selection"],canvas:["build-canvas","canvas"],idef0:["build-model","model"],spec:["build-spec","spec"]};
async function rerun(stage){
const [ep,key]=RUN[stage];
const btn=document.getElementById(`runbtn-${stage}`);
if(btn){btn.disabled=true;btn.innerHTML=`<span class="spin">⏳</span> Елена анализирует...`}
unapprove(stage);
try{
const r=await fetch(`${API}/api/${ep}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current})});
const d=await r.json();
if(d.error){alert("Ошибка: "+d.error);if(btn){btn.disabled=false;btn.textContent="Повторить"}return}
state[key]=d[key]||d.model||d.selection||d.canvas||d.spec;
await loadProjects();renderMain();
}catch(e){alert("Ошибка: "+e.message);if(btn){btn.disabled=false}}
}
loadProjects();
</script>
</body>
</html>