caterium-app/public/core/developer-console-v1768.js
pavlov346346-source 4ba730990b
Caterium v17.6.8 — Developer Console UX and error fixes
* Add v17.6.8 Developer Console UX and race fixes

* Add Developer Console numbering delete and grouped error RPCs

* Add one-time v17.6.8 release preparation workflow

* Prepare Caterium v17.6.8 Developer Console UX

* Remove one-time v17.6.8 release workflow

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-08 07:58:54 +03:00

153 lines
33 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';
if(window.SunDeveloperUXV1768)return;
const VERSION='17.6.8';
const RELEASE='20260908-v17-6-8-developer-console-ux';
const TAB_KEY='sunDeveloperActiveTabV22';
const ENHANCED_TABS=new Set(['companies','accounts','plans','journal']);
const ASYNC_ROOTS={features:'sunDevFeatureBody',backups:'sunDevBackupBody'};
const FEATURES={orders:'Заказы',calendar:'Календарь',map:'Карта',clients:'Клиенты',catalog_view:'Каталог · просмотр',catalog_edit:'Каталог · редактирование',production:'Производство',shopping:'Закупки',stock:'Склад',routes:'Маршруты',mailings:'Рассылки',money:'Финансы',stats_basic:'Статистика',stats_advanced:'Расширенная статистика',team:'Команда',suppliers:'Поставщики',print:'Печать',settings:'Настройки',branding:'Фирменное оформление',client_offers:'Предложение клиенту',offer_templates:'Шаблоны предложения',backups:'Резервные копии',audit:'Журнал',users_manage:'Пользователи и права'};
const PLANS={basic:'Базовый',professional:'Профессиональный',full:'Полный'};
const KNOWN_DOM_RACE=/Cannot set properties of null \(setting 'className'\)|Cannot read properties of undefined \(reading 'add'\)/i;
const $=id=>document.getElementById(id);
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const cloud=()=>window.SunCloudV2||null;
const client=()=>cloud()?.getClient?.()||null;
const dev=()=>window.SunDeveloperV22||null;
const fmtDate=v=>{if(!v)return'—';try{return new Date(v).toLocaleString('ru-RU',{dateStyle:'short',timeStyle:'short'})}catch(_){return String(v)}};
const fmtDay=v=>{if(!v)return'—';try{return new Date(v).toLocaleDateString('ru-RU')}catch(_){return String(v)}};
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}console[type==='error'?'error':'log'](text)};
const activeTab=()=>document.querySelector('#sun-developer-console-v22 [data-dev-tab].on')?.dataset.devTab||localStorage.getItem(TAB_KEY)||'overview';
const developerVisible=()=>Boolean($('sun-developer-console-v22')?.classList?.contains('on'));
async function rpc(name,args={}){const c=client();if(!c)throw new Error('Supabase не подключён.');const r=await c.rpc(name,args);if(r.error)throw r.error;return r.data}
async function audit(action,workspace=null,user=null,details={}){try{await rpc('sun_dev_log_event',{p_action:action,p_workspace:workspace,p_user:user,p_details:details})}catch(_){}}
function installStyles(){
if($('sunDeveloperUXV1768Style'))return;
const st=document.createElement('style');st.id='sunDeveloperUXV1768Style';st.textContent=`
.sun-dev-number{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:26px;padding:0 8px;border-radius:999px;background:#171815;color:#fff;font-size:11px;font-weight:900;white-space:nowrap}
.sun-dev-company-title{display:flex;align-items:center;gap:8px}.sun-dev-company-title b{font-size:14px}
.sun-dev-company-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}.sun-dev-company-pick{border:1px solid #e0e3e7;border-radius:15px;background:#fff;padding:15px;text-align:left;cursor:pointer;box-shadow:0 6px 22px rgba(20,22,26,.035)}.sun-dev-company-pick:hover{border-color:#b99a5f;box-shadow:0 8px 28px rgba(20,22,26,.07)}.sun-dev-company-pick h3{display:flex;align-items:center;gap:8px;margin:0 0 8px;color:#252b2f}.sun-dev-company-pick p{margin:4px 0;color:#747b83;font-size:12px}.sun-dev-company-pick .count{font-size:22px;font-weight:900;color:#171815;margin-top:10px}
.sun-dev-account-head{display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap;margin-bottom:12px}.sun-dev-account-head-left{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.sun-dev-account-role{display:inline-flex;padding:4px 8px;border-radius:999px;background:#f1f3f5;font-size:11px;font-weight:800}.sun-dev-account-role.admin{background:#e9f5ec;color:#22633b}
.sun-dev-plan-top{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.sun-dev-plan-mini{background:#fff;border:1px solid #e2e4e9;border-radius:14px;padding:13px}.sun-dev-plan-mini h3{margin:0 0 3px}.sun-dev-plan-mini p{margin:0 0 9px;color:#777d86;font-size:11px;min-height:16px}.sun-dev-plan-limit{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px;align-items:end}.sun-dev-plan-limit input{width:100%;box-sizing:border-box;padding:8px 9px;border:1px solid #d8dbe0;border-radius:8px}.sun-dev-plan-matrix{min-width:620px}.sun-dev-plan-matrix th:not(:first-child),.sun-dev-plan-matrix td:not(:first-child){text-align:center;width:150px}.sun-dev-plan-matrix input[type=checkbox]{width:18px;height:18px;accent-color:#171815}.sun-dev-plan-feature{font-weight:750;color:#31373c}
.sun-dev-journal-kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.sun-dev-journal-ok{border:1px solid #cae8d5;background:#f2fbf5;border-radius:15px;padding:18px}.sun-dev-journal-ok h3{margin:0 0 5px;color:#17633a}.sun-dev-error-message{max-width:560px;white-space:normal;word-break:break-word}.sun-dev-error-count{font-size:12px;font-weight:900;padding:4px 8px;border-radius:999px;background:#f1f2f4;white-space:nowrap}.sun-dev-error-level{display:inline-flex;padding:4px 8px;border-radius:999px;font-size:10px;font-weight:900;text-transform:uppercase}.sun-dev-error-level.error{background:#fde9e9;color:#9e2828}.sun-dev-error-level.warn{background:#fff3dc;color:#855800}
.sun-dev-modal-v1768{position:fixed;inset:0;z-index:100100;background:rgba(20,22,26,.54);display:grid;place-items:center;padding:18px}.sun-dev-modal-v1768-card{width:min(620px,100%);max-height:90vh;overflow:auto;background:#fff;border-radius:18px;padding:20px;box-shadow:0 24px 80px rgba(0,0,0,.28)}.sun-dev-modal-v1768-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sun-dev-modal-v1768-head h2{margin:0}.sun-dev-modal-v1768-close{border:0;border-radius:10px;width:36px;height:36px;background:#f1f2f4;font-size:22px}.sun-dev-modal-v1768-card label{display:block;margin-top:10px}.sun-dev-modal-v1768-card input,.sun-dev-modal-v1768-card select{width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid #d6d9df;border-radius:9px}.sun-dev-modal-v1768-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-top:16px}
@media(max-width:900px){.sun-dev-plan-top,.sun-dev-journal-kpis{grid-template-columns:1fr}.sun-dev-plan-mini p{min-height:0}}
`;document.head.appendChild(st);
}
function modal(title,html){
$('sunDevModalV1768')?.remove();
const host=document.createElement('div');host.id='sunDevModalV1768';host.className='sun-dev-modal-v1768';host.innerHTML=`<div class="sun-dev-modal-v1768-card"><div class="sun-dev-modal-v1768-head"><h2>${esc(title)}</h2><button class="sun-dev-modal-v1768-close" type="button" data-close>×</button></div><div style="margin-top:12px">${html}</div></div>`;document.body.appendChild(host);
const close=()=>host.remove();host.addEventListener('click',e=>{if(e.target===host||e.target.closest('[data-close]'))close()});return {host,close,body:host.querySelector('.sun-dev-modal-v1768-card')};
}
function setTabChrome(tab){localStorage.setItem(TAB_KEY,tab);qa('#sun-developer-console-v22 [data-dev-tab]').forEach(b=>b.classList.toggle('on',b.dataset.devTab===tab))}
function body(){return $('sunDevBody')}
async function getCompanies(){
let rows;
try{rows=await rpc('sun_dev_list_companies_v1768')}catch(_){rows=await rpc('sun_dev_list_companies')}
rows=Array.isArray(rows)?rows:[];
const sorted=[...rows].sort((a,b)=>new Date(a.created_at||0)-new Date(b.created_at||0)||String(a.workspace_id).localeCompare(String(b.workspace_id)));
sorted.forEach((r,i)=>{if(!Number(r.company_number))r.company_number=i+1});
return sorted.sort((a,b)=>Number(a.company_number||0)-Number(b.company_number||0));
}
async function getUsers(){const rows=await rpc('sun_dev_list_users');return Array.isArray(rows)?rows:[]}
async function renderCompanies(root){
const rows=await getCompanies();if(!root?.isConnected||activeTab()!=='companies')return;
root.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск<input id="sunDevCompanySearchV1768" placeholder="№, название или email владельца"></label><button class="primary" id="sunDevCreateCompanyV1768" type="button">Создать компанию</button></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>№</th><th>Компания</th><th>Владелец</th><th>Тариф</th><th>Доступ</th><th>Сотрудники</th><th>Создана</th><th>Действия</th></tr></thead><tbody id="sunDevCompaniesTbodyV1768"></tbody></table></div>`;
const draw=()=>{
const tbody=$('sunDevCompaniesTbodyV1768');if(!tbody)return;
const q=String($('sunDevCompanySearchV1768')?.value||'').trim().toLowerCase();
const filtered=rows.filter(r=>`${r.company_number||''} ${r.workspace_name||''} ${r.owner_email||''}`.toLowerCase().includes(q));
tbody.innerHTML=filtered.map(r=>`<tr data-ws="${esc(r.workspace_id)}"><td><span class="sun-dev-number">№${Number(r.company_number||0)}</span></td><td><div class="sun-dev-company-title"><b>${esc(r.workspace_name||'Компания')}</b></div><div class="sun-dev-muted">${esc(r.workspace_id)}</div></td><td>${esc(r.owner_email||r.pending_owner_email||'—')}${r.pending_owner_email?'<div class="sun-dev-pill warn">ожидает регистрации</div>':''}</td><td>${esc(PLANS[r.plan_id]||r.plan_name||r.plan_id||'—')}<div class="sun-dev-muted">до ${fmtDay(r.trial_ends_at||r.current_period_end)}</div></td><td><span class="sun-dev-pill ${r.access_mode==='full'?'ok':r.access_mode==='blocked'?'bad':'warn'}">${esc(r.access_mode||r.status||'—')}</span></td><td>${Number(r.member_count||0)} / ${r.max_members==null?'∞':Number(r.max_members)}</td><td>${fmtDay(r.created_at)}</td><td><div class="sun-dev-actions"><button class="outline" data-support>Поддержка</button><button class="outline" data-diag>Диагностика</button><button class="outline" data-plan>Тариф</button><button class="outline sun-dev-danger" data-block>${r.access_mode==='blocked'?'Разблокировать':'Заблокировать'}</button><button class="outline sun-dev-danger" data-delete>Удалить</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="sun-dev-empty">Компаний не найдено.</td></tr>';
qa('tr[data-ws]',tbody).forEach(tr=>{const row=rows.find(x=>String(x.workspace_id)===tr.dataset.ws);tr.querySelector('[data-support]').onclick=()=>enterSupport(row);tr.querySelector('[data-diag]').onclick=()=>showDiagnostics(row);tr.querySelector('[data-plan]').onclick=()=>showCompanyPlan(row);tr.querySelector('[data-block]').onclick=()=>toggleCompanyBlock(row);tr.querySelector('[data-delete]').onclick=()=>deleteCompany(row)});
};
draw();$('sunDevCompanySearchV1768').oninput=draw;$('sunDevCreateCompanyV1768').onclick=createCompany;
}
function createCompany(){
const ui=modal('Создать компанию',`<label>Название<input id="sunDevNewCompanyNameV1768" value="Новая компания"></label><label>Email владельца<input id="sunDevNewOwnerEmailV1768" type="email" placeholder="owner@example.com"></label><div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><label>Тариф<select id="sunDevNewPlanV1768"><option value="basic">Базовый</option><option value="professional">Профессиональный</option><option value="full" selected>Полный</option></select></label><label>Срок, дней<input id="sunDevNewDaysV1768" type="number" min="1" max="3650" value="30"></label></div><p class="sun-dev-muted">Номер присвоится автоматически и больше не изменится, даже если другие компании будут удалены.</p><div class="sun-dev-modal-v1768-actions"><button class="outline" data-close>Отмена</button><button class="primary" id="sunDevNewCreateV1768">Создать</button></div><div id="sunDevNewErrorV1768" class="sun-dev-muted"></div>`);
$('sunDevNewCreateV1768').onclick=async()=>{const name=String($('sunDevNewCompanyNameV1768')?.value||'').trim(),email=String($('sunDevNewOwnerEmailV1768')?.value||'').trim(),plan=$('sunDevNewPlanV1768')?.value||'full',days=Math.max(1,Number($('sunDevNewDaysV1768')?.value||30));if(!name){$('sunDevNewErrorV1768').textContent='Введите название.';return}const btn=$('sunDevNewCreateV1768');btn.disabled=true;$('sunDevNewErrorV1768').textContent='Создаю…';try{await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:Array.isArray(window.SUN_OFFICIAL_CATALOG)?window.SUN_OFFICIAL_CATALOG:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.close();toast('Компания создана и получила следующий номер.','success');renderEnhancedTab('companies')}catch(e){btn.disabled=false;$('sunDevNewErrorV1768').textContent=e?.message||String(e)}};
}
async function deleteCompany(row){
if(!row)return;const name=String(row.workspace_name||'');
if(!confirm(`Удалить компанию №${Number(row.company_number||0)} «${name}»?\n\nБудут удалены её заказы, клиенты, каталог, настройки, чаты и резервные копии. Сами аккаунты пользователей из Auth удаляться не будут.`))return;
const typed=prompt(`Для подтверждения введите точное название компании:\n${name}`,'');if(typed===null)return;if(String(typed).trim()!==name){toast('Удаление отменено: название не совпало.','warn');return}
try{await rpc('sun_dev_delete_company_v1768',{p_workspace:row.workspace_id,p_confirm_name:name});toast(`Компания №${Number(row.company_number||0)} удалена. Её номер больше не будет использоваться.`,`success`,6500);renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error',6500)}
}
async function toggleCompanyBlock(row){const blocked=row?.access_mode==='blocked';if(!blocked&&!confirm(`Заблокировать компанию «${row.workspace_name}»? Данные не удалятся.`))return;try{await rpc('sun_dev_set_subscription',{p_workspace:row.workspace_id,p_plan:row.plan_id||'full',p_days:blocked?30:0,p_status:blocked?'active':'expired'});await audit(blocked?'company.unblock':'company.block',row.workspace_id,null,{company:row.workspace_name});toast(blocked?'Компания разблокирована.':'Компания заблокирована.','success');renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error')}}
function showCompanyPlan(row){const ui=modal(`Тариф · №${Number(row.company_number||0)} ${row.workspace_name}`,`<label>Тариф<select id="sunDevPlanSelectV1768"><option value="basic" ${row.plan_id==='basic'?'selected':''}>Базовый</option><option value="professional" ${row.plan_id==='professional'?'selected':''}>Профессиональный</option><option value="full" ${row.plan_id==='full'?'selected':''}>Полный</option></select></label><label>Продлить на, дней<select id="sunDevPlanDaysV1768"><option value="30">30</option><option value="90">90</option><option value="365">365</option></select></label><div class="sun-dev-modal-v1768-actions"><button class="outline" data-close>Отмена</button><button class="primary" id="sunDevPlanSaveV1768">Применить</button></div>`);$('sunDevPlanSaveV1768').onclick=async()=>{try{const plan=$('sunDevPlanSelectV1768').value,days=Number($('sunDevPlanDaysV1768').value);await rpc('sun_dev_set_subscription',{p_workspace:row.workspace_id,p_plan:plan,p_days:days,p_status:'active'});await audit('company.subscription.set',row.workspace_id,null,{plan,days});ui.close();toast('Тариф обновлён.','success');renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error')}}}
async function showDiagnostics(row){try{const d=await rpc('sun_dev_workspace_diagnostics',{p_workspace:row.workspace_id});modal(`Диагностика · №${Number(row.company_number||0)} ${row.workspace_name}`,`<div class="sun-dev-grid" style="grid-template-columns:repeat(2,minmax(0,1fr))"><div class="sun-dev-kpi"><small>Заказы</small><b>${Number(d.orders||0)}</b></div><div class="sun-dev-kpi"><small>Клиенты</small><b>${Number(d.clients||0)}</b></div><div class="sun-dev-kpi"><small>Каталог</small><b>${Number(d.catalog_items||0)}</b></div><div class="sun-dev-kpi"><small>Ошибки 24 ч.</small><b>${Number(d.errors_24h||0)}</b></div></div><p><b>Revision:</b> ${Number(d.revision||0)}</p><p><b>Обновлено:</b> ${fmtDate(d.state_updated_at)}</p><p><b>Последняя копия:</b> ${fmtDate(d.latest_backup)}</p><p><b>Режим доступа:</b> ${esc(d.access_mode||'—')}</p>`)}catch(e){toast(e?.message||String(e),'error')}}
async function enterSupport(row){if(!row)return;try{await cloud()?.enterSupportWorkspace?.(row.workspace_id,row.workspace_name);await audit('support.ui_enter',row.workspace_id,null,{company:row.workspace_name});for(const fn of ['render','renderOrders','renderStats']){try{window[fn]?.()}catch(_){}}const orders=qa('header nav button').find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Заказы');orders?.click();toast('Режим поддержки открыт. Изменения не будут сохранены.','info',6500)}catch(e){toast(e?.message||String(e),'error')}}
let selectedAccountsWorkspace=null;
async function renderAccounts(root){
const [companies,rows]=await Promise.all([getCompanies(),getUsers()]);if(!root?.isConnected||activeTab()!=='accounts')return;
const userMap=new Map();for(const r of rows){const id=String(r.user_id);if(!userMap.has(id))userMap.set(id,{id,email:r.email,created_at:r.created_at,last_sign_in_at:r.last_sign_in_at,is_platform_admin:r.is_platform_admin===true,workspaces:[]});if(r.workspace_id)userMap.get(id).workspaces.push({id:r.workspace_id,name:r.workspace_name,role:r.role,display_name:r.display_name,is_active:r.is_active!==false})}
const users=[...userMap.values()];
const groups=companies.map(c=>({...c,users:users.filter(u=>u.workspaces.some(w=>String(w.id)===String(c.workspace_id)))}));
const orphan=users.filter(u=>!(u.workspaces||[]).length);
const selected=selectedAccountsWorkspace==='__orphan'?{workspace_id:'__orphan',company_number:null,workspace_name:'Без компании',users:orphan}:groups.find(g=>String(g.workspace_id)===String(selectedAccountsWorkspace));
if(selected){
root.innerHTML=`<div class="sun-dev-account-head"><div class="sun-dev-account-head-left"><button class="outline" id="sunDevAccountsBackV1768" type="button">← Компании</button>${selected.company_number?`<span class="sun-dev-number">№${Number(selected.company_number)}</span>`:''}<div><h2 style="margin:0">${esc(selected.workspace_name)}</h2><div class="sun-dev-muted">${selected.users.length} ${selected.users.length===1?'аккаунт':'аккаунтов'}</div></div></div></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Аккаунт</th><th>Роль в компании</th><th>Регистрация</th><th>Последний вход</th><th>Статус</th></tr></thead><tbody>${selected.users.map(u=>{const memberships=u.workspaces.filter(w=>selected.workspace_id==='__orphan'||String(w.id)===String(selected.workspace_id));return `<tr><td><b>${esc(u.email||'—')}</b>${u.is_platform_admin?'<div><span class="sun-dev-pill ok">Разработчик</span></div>':''}<div class="sun-dev-muted">${esc(u.id)}</div></td><td>${memberships.length?memberships.map(w=>`<span class="sun-dev-account-role ${w.role==='admin'?'admin':''}">${esc(w.role||'—')}</span>${w.display_name?`<div class="sun-dev-muted">${esc(w.display_name)}</div>`:''}`).join(''):'—'}</td><td>${fmtDate(u.created_at)}</td><td>${fmtDate(u.last_sign_in_at)}</td><td>${memberships.some(w=>w.is_active===false)?'<span class="sun-dev-pill warn">неактивен</span>':'<span class="sun-dev-pill ok">активен</span>'}</td></tr>`}).join('')||'<tr><td colspan="5" class="sun-dev-empty">В этой компании аккаунтов пока нет.</td></tr>'}</tbody></table></div>`;
$('sunDevAccountsBackV1768').onclick=()=>{selectedAccountsWorkspace=null;renderEnhancedTab('accounts')};return;
}
root.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск компании<input id="sunDevAccountsCompanySearchV1768" placeholder="№ или название компании"></label><span class="sun-dev-pill">${users.length} аккаунтов всего</span></div><div id="sunDevAccountsCompanyGridV1768" class="sun-dev-company-grid"></div>`;
const draw=()=>{const grid=$('sunDevAccountsCompanyGridV1768');if(!grid)return;const q=String($('sunDevAccountsCompanySearchV1768')?.value||'').trim().toLowerCase();const list=groups.filter(g=>`${g.company_number||''} ${g.workspace_name||''}`.toLowerCase().includes(q));grid.innerHTML=list.map(g=>`<button class="sun-dev-company-pick" type="button" data-account-company="${esc(g.workspace_id)}"><h3><span class="sun-dev-number">№${Number(g.company_number||0)}</span>${esc(g.workspace_name)}</h3><p>${esc(g.owner_email||'Владелец не указан')}</p><div class="count">${g.users.length}</div><p>${g.users.length===1?'созданный аккаунт':'созданных аккаунтов'}</p></button>`).join('')+(orphan.length?`<button class="sun-dev-company-pick" type="button" data-account-company="__orphan"><h3>Без компании</h3><p>Системные или ещё не привязанные аккаунты</p><div class="count">${orphan.length}</div><p>аккаунтов</p></button>`:'');qa('[data-account-company]',grid).forEach(b=>b.onclick=()=>{selectedAccountsWorkspace=b.dataset.accountCompany;renderEnhancedTab('accounts')})};draw();$('sunDevAccountsCompanySearchV1768').oninput=draw;
}
async function renderPlans(root){
const c=client();if(!c)throw new Error('Supabase не подключён.');const [plansR,featuresR]=await Promise.all([c.from('sun_plans').select('id,name,description,max_members,is_active,sort_order').order('sort_order'),c.from('sun_plan_features').select('plan_id,feature_key,enabled')]);if(plansR.error)throw plansR.error;if(featuresR.error)throw featuresR.error;if(!root?.isConnected||activeTab()!=='plans')return;
const plans=plansR.data||[],rows=featuresR.data||[],featureKeys=[...new Set([...Object.keys(FEATURES),...rows.map(r=>r.feature_key)])];
root.innerHTML=`<div class="sun-dev-plan-top">${plans.map(p=>`<div class="sun-dev-plan-mini" data-plan-mini="${esc(p.id)}"><h3>${esc(p.name)}</h3><p>${esc(p.description||'')}</p><div class="sun-dev-plan-limit"><label style="margin:0">Сотрудники<input data-plan-max type="number" min="1" placeholder="∞" value="${p.max_members??''}"></label><button class="outline" data-plan-max-save type="button">Сохранить</button></div></div>`).join('')}</div><div class="sun-dev-table-wrap"><table class="sun-dev-table sun-dev-plan-matrix"><thead><tr><th>Функция</th>${plans.map(p=>`<th>${esc(p.name)}</th>`).join('')}</tr></thead><tbody>${featureKeys.map(f=>`<tr><td class="sun-dev-plan-feature">${esc(FEATURES[f]||f)}</td>${plans.map(p=>{const row=rows.find(x=>x.plan_id===p.id&&x.feature_key===f);return `<td><input type="checkbox" data-plan-feature="${esc(p.id)}" data-feature="${esc(f)}" ${row?.enabled?'checked':''} aria-label="${esc(p.name)}${esc(FEATURES[f]||f)}"></td>`}).join('')}</tr>`).join('')}</tbody></table></div><p class="sun-dev-muted" style="margin-top:10px">Матрица компактная: строка — функция, столбец — тариф. Изменения чекбоксов сохраняются сразу.</p>`;
qa('[data-plan-mini]',root).forEach(card=>{card.querySelector('[data-plan-max-save]').onclick=async()=>{const raw=card.querySelector('[data-plan-max]').value;try{await rpc('sun_dev_set_plan_max_members',{p_plan:card.dataset.planMini,p_max_members:raw===''?null:Number(raw)});toast('Лимит сотрудников сохранён.','success')}catch(e){toast(e?.message||String(e),'error')}}});
qa('[data-plan-feature]',root).forEach(ch=>ch.onchange=async()=>{ch.disabled=true;try{await rpc('sun_dev_set_plan_feature',{p_plan:ch.dataset.planFeature,p_feature:ch.dataset.feature,p_enabled:ch.checked});toast('Функция тарифа обновлена.','success',2500)}catch(e){ch.checked=!ch.checked;toast(e?.message||String(e),'error')}finally{ch.disabled=false}});
}
async function errorGroups(){
try{return await rpc('sun_dev_error_groups_v1768',{p_hours:168,p_limit:100})||[]}catch(_){const raw=await rpc('sun_dev_list_errors',{p_workspace:null,p_limit:100})||[],map=new Map();for(const e of raw){const key=`${e.workspace_name||''}|${e.app_version||''}|${e.level||''}|${e.message||''}`;if(!map.has(key))map.set(key,{workspace_name:e.workspace_name,app_version:e.app_version,level:e.level,message:e.message,occurrences:0,first_seen:e.created_at,last_seen:e.created_at});const g=map.get(key);g.occurrences++;if(new Date(e.created_at)<new Date(g.first_seen))g.first_seen=e.created_at;if(new Date(e.created_at)>new Date(g.last_seen))g.last_seen=e.created_at}return [...map.values()]}
}
async function renderJournal(root){
const [activity,groups]=await Promise.all([rpc('sun_dev_list_activity',{p_limit:120}),errorGroups()]);if(!root?.isConnected||activeTab()!=='journal')return;
const prod=(groups||[]).filter(g=>String(g.source_url||'').startsWith('file:')===false),errors=prod.filter(g=>String(g.level||'error').toLowerCase()==='error'),warns=prod.filter(g=>String(g.level||'').toLowerCase()!=='error'),total=prod.reduce((s,g)=>s+Number(g.occurrences||0),0);
root.innerHTML=`<div class="sun-dev-journal-kpis"><div class="sun-dev-kpi"><small>Актуальные типы ошибок · 7 дней</small><b>${errors.length}</b></div><div class="sun-dev-kpi"><small>Предупреждения · 7 дней</small><b>${warns.length}</b></div><div class="sun-dev-kpi"><small>События этих типов</small><b>${total}</b></div></div>${prod.length?`<div class="sun-dev-card" style="padding:0;overflow:hidden"><div style="padding:14px 16px 5px"><h2>Актуальные ошибки приложения</h2><p class="sun-dev-muted">Повторы сгруппированы. Старые локальные file:// копии больше не попадают в этот журнал.</p></div><div class="sun-dev-table-wrap" style="border:0;border-radius:0"><table class="sun-dev-table"><thead><tr><th>Последний раз</th><th>Компания</th><th>Уровень</th><th>Кол-во</th><th>Версия</th><th>Сообщение</th></tr></thead><tbody>${prod.map(g=>`<tr><td>${fmtDate(g.last_seen)}</td><td>${esc(g.workspace_name||'—')}</td><td><span class="sun-dev-error-level ${String(g.level||'error').toLowerCase()==='error'?'error':'warn'}">${esc(g.level||'error')}</span></td><td><span class="sun-dev-error-count">${Number(g.occurrences||0)}</span></td><td>${esc(g.app_version||'—')}</td><td class="sun-dev-error-message"><b>${esc(g.message||'')}</b>${g.sample_stack?`<details><summary class="sun-dev-muted">Стек</summary><pre class="sun-dev-pre">${esc(g.sample_stack)}</pre></details>`:''}</td></tr>`).join('')}</tbody></table></div></div>`:`<div class="sun-dev-journal-ok"><h3>Актуальных ошибок нет</h3><div>За последние 7 дней production-журнал чист. Старые ошибки от локальных копий отфильтрованы на сервере.</div></div>`}<details class="sun-dev-card" style="margin-top:12px"><summary style="cursor:pointer;font-weight:850">Действия разработчика · ${(activity||[]).length}</summary><div class="sun-dev-table-wrap" style="margin-top:12px"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Действие</th><th>Компания / аккаунт</th><th>Детали</th></tr></thead><tbody>${(activity||[]).map(a=>`<tr><td>${fmtDate(a.created_at)}</td><td><b>${esc(a.action)}</b><div class="sun-dev-muted">${esc(a.actor_email||'')}</div></td><td>${esc(a.workspace_name||a.target_email||'—')}</td><td class="sun-dev-muted">${esc(JSON.stringify(a.details||{}))}</td></tr>`).join('')||'<tr><td colspan="4">Записей пока нет.</td></tr>'}</tbody></table></div></details>`;
}
async function renderEnhancedTab(tab){
if(!ENHANCED_TABS.has(tab))return false;const root=body();if(!root)return false;setTabChrome(tab);root.innerHTML='<div class="sun-dev-empty">Загрузка…</div>';
try{if(tab==='companies')await renderCompanies(root);else if(tab==='accounts')await renderAccounts(root);else if(tab==='plans')await renderPlans(root);else if(tab==='journal')await renderJournal(root)}catch(e){if(root.isConnected&&activeTab()===tab)root.innerHTML=`<div class="sun-dev-card"><h3>Не удалось загрузить раздел</h3><p>${esc(e?.message||e)}</p></div>`;toast(e?.message||String(e),'error')};return true;
}
function sentinel(id){let el=$(id);if(el)return el;el=document.createElement('div');el.id=id;el.hidden=true;el.dataset.sunV1768Sentinel='1';document.body.appendChild(el);return el}
function removeSentinel(id){const el=$(id);if(el?.dataset?.sunV1768Sentinel==='1')el.remove()}
function prepareAsyncRoots(nextTab){const current=activeTab();for(const [tab,id] of Object.entries(ASYNC_ROOTS)){if(nextTab===tab)removeSentinel(id);else if(current===tab)queueMicrotask(()=>{if(activeTab()!==tab)sentinel(id)})}}
let installed=false,originalOpen=null,originalActivate=null;
function patchDeveloper(){
const d=dev();if(!d||installed)return false;installed=true;originalOpen=typeof d.open==='function'?d.open.bind(d):null;originalActivate=typeof d.activateTab==='function'?d.activateTab.bind(d):null;
if(originalOpen){d.open=async source=>{const wanted=localStorage.getItem(TAB_KEY)||'overview',enhanced=ENHANCED_TABS.has(wanted),safeSource=source instanceof HTMLElement?source:null;if(enhanced)localStorage.setItem(TAB_KEY,'overview');let result;try{result=await originalOpen(safeSource)}catch(e){if(!KNOWN_DOM_RACE.test(String(e?.message||e)))throw e;console.warn('[Caterium v17.6.8] Developer DOM race recovered',e);localStorage.setItem(TAB_KEY,'overview');try{result=await originalOpen(null)}catch(second){throw second}}finally{if(enhanced)localStorage.setItem(TAB_KEY,wanted)}if(enhanced)await renderEnhancedTab(wanted);return result};Object.defineProperty(d,'__sunV1768SafeOpen',{value:true,configurable:true})}
if(originalActivate){d.activateTab=async(tab,force=false)=>ENHANCED_TABS.has(tab)?renderEnhancedTab(tab):originalActivate(tab,force)}
return true;
}
function patchToast(){const e=window.SunEnterprise;if(!e||e.__sunV1768Toast||typeof e.toast!=='function')return;const original=e.toast.bind(e);e.toast=(text,...args)=>{const msg=String(text||'');if(developerVisible()&&KNOWN_DOM_RACE.test(msg)){console.warn('[Caterium v17.6.8] suppressed recovered Developer DOM race:',msg);return null}return original(text,...args)};Object.defineProperty(e,'__sunV1768Toast',{value:true,configurable:true})}
function recoverError(event,error){const msg=String(error?.message||error||'');if(!developerVisible()||!KNOWN_DOM_RACE.test(msg))return false;console.warn('[Caterium v17.6.8] recovered Developer Console DOM race:',error);event?.preventDefault?.();event?.stopImmediatePropagation?.();const tab=activeTab();setTimeout(()=>{if(ENHANCED_TABS.has(tab))renderEnhancedTab(tab);else dev()?.activateTab?.(tab,true)?.catch?.(()=>{})},60);return true}
function onClick(e){
const tabBtn=e.target?.closest?.('#sun-developer-console-v22 [data-dev-tab]');
if(tabBtn){const next=tabBtn.dataset.devTab;prepareAsyncRoots(next);if(ENHANCED_TABS.has(next)){e.preventDefault();e.stopImmediatePropagation();renderEnhancedTab(next);return}}
const refresh=e.target?.closest?.('#sunDevRefresh');if(refresh&&ENHANCED_TABS.has(activeTab())){e.preventDefault();e.stopImmediatePropagation();renderEnhancedTab(activeTab())}
}
document.addEventListener('click',onClick,true);
window.addEventListener('unhandledrejection',e=>recoverError(e,e.reason));
window.addEventListener('error',e=>recoverError(e,e.error||e.message));
function boot(){installStyles();patchToast();if(!patchDeveloper()){let tries=0;const timer=setInterval(()=>{tries++;patchToast();if(patchDeveloper()||tries>40)clearInterval(timer)},400)}setInterval(()=>{if(!document.hidden){patchToast();patchDeveloper()}},10000)}
window.SunDeveloperUXV1768={VERSION,RELEASE,renderEnhancedTab,getCompanies,getUsers,patchDeveloper,checks:()=>({version:VERSION,installed,activeTab:activeTab(),visible:developerVisible()})};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
})();