feat(crm): индикатор баланса AI-движка

/api/ai-status — пинг в 1 токен, ловит low_balance. В CRM: точка в шапке
(зелёная/красная) + плашка «AI-движок недоступен — пополните баланс».
Без названия вендора (публичный репо + правило конфиденциальности).
This commit is contained in:
wasrusgen 2026-06-02 06:28:15 +03:00
parent ca9bdc8e0c
commit 835b6a05df
2 changed files with 27 additions and 1 deletions

View File

@ -247,6 +247,17 @@ def run_tool(project_id, tool, tool_name, instruction, extra_context=None, max_t
def health():
return jsonify({"ok": True, "model": MODEL, "time": now()})
@app.route("/api/ai-status")
def ai_status():
"""Лёгкая проверка доступности AI-движка (пинг в 1 токен). Не бросает — всегда 200."""
try:
client.messages.create(model=MODEL, max_tokens=1, messages=[{"role": "user", "content": "ping"}])
return jsonify({"ok": True})
except Exception as e:
msg = str(e)
low = ("credit balance" in msg.lower()) or ("too low" in msg.lower()) or ("billing" in msg.lower())
return jsonify({"ok": False, "reason": "low_balance" if low else "error", "detail": msg[:200]})
@app.route("/api/project/new", methods=["POST"])
def new_project():
data = request.get_json(force=True) or {}

View File

@ -161,7 +161,8 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
</style>
</head>
<body>
<header class="hdr"><button class="hdr-burger" onclick="toggleSb()" aria-label="Меню"></button><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)"></span>Руслан</div></header>
<header class="hdr"><button class="hdr-burger" onclick="toggleSb()" aria-label="Меню"></button><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 id="aiDot" title="AI-движок Елены" style="width:8px;height:8px;border-radius:50%;background:#9CA3AF"></span>Руслан</div></header>
<div id="aiBanner" style="display:none;align-items:center;gap:10px;background:#FEF2F2;border-bottom:1px solid #FCA5A5;color:#991B1B;padding:9px 18px;font-size:13px;font-weight:600"><span id="aiBannerText">AI-движок недоступен</span></div>
<div class="layout">
<div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
<aside class="sb" id="sbNav">
@ -1106,7 +1107,21 @@ function renderCanvas(c){const B=(k,cls,l)=>{const b=c[k];return `<div class="cv
function renderIdef(m){const box=(fn,ct,ins,outs,me,id,pct)=>{const C=(ct&&ct.length)?ct.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=(me||[]).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:10px;padding:11px 15px;margin-bottom:12px;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:10px 13px"><div style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)} · ${g.type}</div><div style="font-size:13px;font-weight:700">${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-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div><div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:11px 14px"><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 class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach((m,mi)=>h+=`<div class="mod"><div style="display:flex;align-items:center;gap:8px;margin-bottom:5px"><span class="mod-node">${esc(m.source_node)}</span><b>${esc(m.name)}</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 11px" onclick="designScreen('${esc(m.name).replace(/'/g,"")}',${mi})">🖼 Экран</button></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:5px 0">${m.screens.map(x=>`<span class="scr">${esc(x)}</span>`).join("")}</div><div style="font-size:12px;color:#374151">📥 ${esc(m.inputs_data)} · 📤 ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:5px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}<div id="screen-${mi}" style="margin-top:10px"></div></div>`);h+=`<div class="spec-h"><span class="pl">C</span>Данные (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><b style="font-family:Montserrat">◆ ${esc(e.name)}</b><div class="ent-fields" style="margin-top:7px">${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:5px">🔗 ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><b>Уточнить перед разработкой</b>${s.open_questions.map(q=>`<div class="blk-pain" style="margin-top:6px">${esc(q)}</div>`).join("")}</div>`}return h;}
async function checkAiStatus(){
try{
const r=await fetch(`${API}/api/ai-status`);const d=await r.json();
const b=document.getElementById('aiBanner'),t=document.getElementById('aiBannerText'),dot=document.getElementById('aiDot');
if(d.ok){b.style.display='none';if(dot){dot.style.background='#10B981';dot.title='AI-движок Елены: в норме';}return;}
if(dot){dot.style.background='#EF4444';dot.title='AI-движок Елены: недоступен';}
t.textContent=d.reason==='low_balance'
?'⚠️ AI-движок Елены недоступен: недостаточно средств на балансе. Пополните баланс — без него Елена не строит стратегию, модель, оргструктуру, должностные и ТЗ.'
:'⚠️ AI-движок Елены временно недоступен (техническая ошибка). Генерация артефактов не работает.';
b.style.display='flex';
}catch(e){/* сетевой сбой — не показываем ложную тревогу */}
}
loadProjects().then(render);
checkAiStatus();
setInterval(checkAiStatus,3600000); // раз в час
</script>
</body>
</html>