diff --git a/docs/release-manifest.json b/docs/release-manifest.json
index b684957..211960e 100644
--- a/docs/release-manifest.json
+++ b/docs/release-manifest.json
@@ -1,6 +1,6 @@
{
"app": "Caterium",
- "version": "v17.6.7",
+ "version": "v17.6.8",
"channel": "production",
"schema": 17,
"legacyStateKept": true,
@@ -11,7 +11,7 @@
"serverReady": true,
"workspaceAutoDiscovery": true,
"invitesTemporarilyDisabled": false,
- "pwaCache": "v72-20260908-v17-6-7-classic-offer-pdf",
+ "pwaCache": "v73-20260908-v17-6-8-developer-console-ux",
"fullOfferDescriptions": true,
"dynamicOfferRows": true,
"pdfOfferDescriptionFix": true,
@@ -108,7 +108,7 @@
"catalogCompositionTildaEndpoint": "getproduct",
"catalogCompositionPremiumForceRefresh": true,
"catalogCompositionCacheRequiresPremium": true,
- "notes": "Restored the earlier presentation-style client PDF experience: 10 classic visual concepts plus 3 hidden archive templates. Preview and download are built from the exact same canvas pages.",
+ "notes": "Developer Console UX: fixed async DOM race errors, stable company numbering and deletion, accounts grouped by company, compact plan matrix, grouped production error journal.",
"catalogPhotoSources": 113,
"catalogPhotosStoredInCatalog": true,
"catalogLegacyPhotosInCatalog": 60,
@@ -233,7 +233,7 @@
"normalUserStabilityHidden": true,
"authAdminEdgeFunctionDeployed": false,
"authAdminEdgeFunctionNote": "Source included, but deployment was unavailable in this build environment. All-account listing uses existing server-side sun_platform_list_users RPC; global Auth ban/recovery controls remain unavailable until protected backend function is deployed.",
- "developerSqlMigration": "SUPABASE-DEVELOPER-V22.sql",
+ "developerSqlMigration": "SUPABASE-V17.6.8-DEVELOPER-CONSOLE-UX.sql",
"developerMfaRecovery": true,
"registrationTabs": true,
"registrationCompanyNameRequired": true,
@@ -246,7 +246,7 @@
"signupTrial": "14-day Full",
"developerMfaInputSelectorFixed": true,
"developerMfaRepeatedValidationToastsFixed": true,
- "release": "20260908-v17-6-7-classic-offer-pdf",
+ "release": "20260908-v17-6-8-developer-console-ux",
"registrationFlow": "email-password-confirm-company-auto-login",
"emailConfirmationRequired": false,
"employeeInviteLinks": false,
@@ -345,5 +345,13 @@
"emerald-gold",
"neon-emerald"
],
- "classicOfferPdfCoverPages": true
+ "classicOfferPdfCoverPages": true,
+ "developerCompanyNumbers": true,
+ "developerCompanyDelete": true,
+ "developerAccountsGroupedByCompany": true,
+ "developerPlansCompactMatrix": true,
+ "developerJournalGroupedErrors": true,
+ "developerJournalFiltersLocalCopies": true,
+ "developerDomRaceFix": true,
+ "developerConsoleUXModule": "core/developer-console-v1768.js"
}
diff --git a/docs/releases/V17.6.8-CHANGES.txt b/docs/releases/V17.6.8-CHANGES.txt
new file mode 100644
index 0000000..c8fa317
--- /dev/null
+++ b/docs/releases/V17.6.8-CHANGES.txt
@@ -0,0 +1,9 @@
+Caterium v17.6.8 — Developer Console UX
+
+- Исправлена гонка DOM при открытии и переключении вкладок кабинета разработчика, вызывавшая className/null и reading add.
+- Компаниям присваивается постоянный последовательный номер, который не переиспользуется после удаления.
+- В разделе «Компании» добавлено безопасное удаление с двойным подтверждением. Связанные данные компании удаляются каскадно, Auth-аккаунты сохраняются.
+- «Аккаунты» сгруппированы по компаниям: сначала выбирается компания, затем видны только её пользователи. Добавлена группа «Без компании».
+- «Тарифы» переведены на компактную матрицу функций вместо длинных повторяющихся списков.
+- «Журнал» группирует одинаковые ошибки и отделяет актуальные production-ошибки от шума старых локальных file:// копий.
+- Добавлен серверный RPC для сгруппированного журнала, постоянной нумерации и удаления компаний.
diff --git a/ops/sql/SUPABASE-V17.6.8-DEVELOPER-CONSOLE-UX.sql b/ops/sql/SUPABASE-V17.6.8-DEVELOPER-CONSOLE-UX.sql
new file mode 100644
index 0000000..3e46834
--- /dev/null
+++ b/ops/sql/SUPABASE-V17.6.8-DEVELOPER-CONSOLE-UX.sql
@@ -0,0 +1,236 @@
+-- Caterium v17.6.8 Developer Console UX
+-- Persistent company numbers, safe company deletion and grouped production error journal.
+
+create sequence if not exists public.sun_workspace_company_number_seq;
+
+alter table public.sun_workspaces
+ add column if not exists company_number bigint;
+
+with numbered as (
+ select id,row_number() over(order by created_at,id)::bigint as n
+ from public.sun_workspaces
+ where company_number is null
+), existing as (
+ select coalesce(max(company_number),0)::bigint as max_n from public.sun_workspaces
+)
+update public.sun_workspaces w
+set company_number = numbered.n + existing.max_n
+from numbered,existing
+where w.id=numbered.id and w.company_number is null;
+
+select setval(
+ 'public.sun_workspace_company_number_seq',
+ greatest(1,coalesce((select max(company_number) from public.sun_workspaces),0)),
+ coalesce((select max(company_number) from public.sun_workspaces),0)>0
+);
+
+alter table public.sun_workspaces
+ alter column company_number set default nextval('public.sun_workspace_company_number_seq');
+
+update public.sun_workspaces
+set company_number=nextval('public.sun_workspace_company_number_seq')
+where company_number is null;
+
+alter table public.sun_workspaces
+ alter column company_number set not null;
+
+create unique index if not exists sun_workspaces_company_number_uq
+ on public.sun_workspaces(company_number);
+
+create or replace function public.sun_dev_list_companies_v1768()
+returns table(
+ workspace_id uuid,
+ workspace_name text,
+ company_number bigint,
+ created_at timestamptz,
+ plan_id text,
+ plan_name text,
+ status text,
+ access_mode text,
+ trial_ends_at timestamptz,
+ current_period_end timestamptz,
+ grace_until timestamptz,
+ member_count bigint,
+ max_members integer,
+ owner_email text,
+ pending_owner_email text
+)
+language plpgsql
+stable
+security definer
+set search_path='public','auth'
+as $$
+begin
+ perform public.sun_require_platform_admin_aal2();
+ return query
+ select
+ w.id,
+ w.name,
+ w.company_number,
+ w.created_at,
+ s.plan_id,
+ p.name,
+ s.status,
+ public.sun_subscription_access_mode(w.id),
+ s.trial_ends_at,
+ s.current_period_end,
+ s.grace_until,
+ (select count(*) from public.sun_workspace_members m where m.workspace_id=w.id and m.is_active),
+ p.max_members,
+ (select u.email::text
+ from public.sun_workspace_members m
+ join auth.users u on u.id=m.user_id
+ where m.workspace_id=w.id and m.role='admin' and m.is_active
+ order by m.created_at asc
+ limit 1),
+ (select i.email
+ from public.caterium_company_owner_invites i
+ where i.workspace_id=w.id and i.used_at is null and i.expires_at>now()
+ order by i.created_at desc
+ limit 1)
+ from public.sun_workspaces w
+ left join public.sun_workspace_subscriptions s on s.workspace_id=w.id
+ left join public.sun_plans p on p.id=s.plan_id
+ order by w.company_number;
+end;
+$$;
+
+create or replace function public.sun_dev_delete_company_v1768(
+ p_workspace uuid,
+ p_confirm_name text
+) returns jsonb
+language plpgsql
+security definer
+set search_path='public','auth'
+as $$
+declare
+ v_name text;
+ v_number bigint;
+ v_members bigint;
+ v_orders bigint;
+ v_clients bigint;
+ v_catalog bigint;
+begin
+ perform public.sun_require_platform_admin_aal2();
+
+ select w.name,w.company_number
+ into v_name,v_number
+ from public.sun_workspaces w
+ where w.id=p_workspace
+ for update;
+
+ if v_name is null then raise exception 'Workspace not found'; end if;
+ if trim(coalesce(p_confirm_name,'')) <> v_name then raise exception 'Company name confirmation mismatch'; end if;
+
+ select count(*) into v_members from public.sun_workspace_members where workspace_id=p_workspace;
+ select count(*) into v_orders from public.sun_v17_orders where workspace_id=p_workspace;
+ select count(*) into v_clients from public.sun_v17_clients where workspace_id=p_workspace;
+ select count(*) into v_catalog from public.sun_v17_catalog_items where workspace_id=p_workspace;
+
+ perform public.sun_platform_log_event(
+ 'company.delete',
+ p_workspace,
+ null,
+ jsonb_build_object(
+ 'company',v_name,
+ 'company_number',v_number,
+ 'members',v_members,
+ 'orders',v_orders,
+ 'clients',v_clients,
+ 'catalog_items',v_catalog,
+ 'auth_accounts_preserved',true
+ )
+ );
+
+ delete from public.sun_workspaces where id=p_workspace;
+
+ return jsonb_build_object(
+ 'deleted',true,
+ 'workspace_id',p_workspace,
+ 'workspace_name',v_name,
+ 'company_number',v_number,
+ 'memberships_removed',v_members,
+ 'auth_accounts_preserved',true
+ );
+end;
+$$;
+
+create or replace function public.sun_dev_error_groups_v1768(
+ p_hours integer default 168,
+ p_limit integer default 100
+) returns table(
+ workspace_name text,
+ app_version text,
+ level text,
+ message text,
+ occurrences bigint,
+ first_seen timestamptz,
+ last_seen timestamptz,
+ sample_stack text,
+ source_url text
+)
+language plpgsql
+stable
+security definer
+set search_path='public'
+as $$
+declare
+ v_hours integer:=greatest(1,least(coalesce(p_hours,168),720));
+ v_limit integer:=greatest(1,least(coalesce(p_limit,100),200));
+begin
+ perform public.sun_require_platform_admin_aal2();
+ return query
+ with scoped as (
+ select
+ e.*,
+ w.name as company_name,
+ nullif(e.context->>'url','') as event_url
+ from public.sun_v17_error_events e
+ left join public.sun_workspaces w on w.id=e.workspace_id
+ where e.created_at >= now()-make_interval(hours=>v_hours)
+ and coalesce(e.context->>'url','') !~* '^file:'
+ ), grouped as (
+ select
+ company_name,
+ coalesce(app_version,'') as app_version,
+ coalesce(level,'error') as level,
+ coalesce(message,'Unknown error') as message,
+ count(*)::bigint as occurrences,
+ min(created_at) as first_seen,
+ max(created_at) as last_seen
+ from scoped
+ group by company_name,coalesce(app_version,''),coalesce(level,'error'),coalesce(message,'Unknown error')
+ )
+ select
+ g.company_name,
+ nullif(g.app_version,''),
+ g.level,
+ g.message,
+ g.occurrences,
+ g.first_seen,
+ g.last_seen,
+ s.stack,
+ s.event_url
+ from grouped g
+ left join lateral (
+ select x.stack,x.event_url
+ from scoped x
+ where x.company_name is not distinct from g.company_name
+ and coalesce(x.app_version,'')=g.app_version
+ and coalesce(x.level,'error')=g.level
+ and coalesce(x.message,'Unknown error')=g.message
+ order by x.created_at desc
+ limit 1
+ ) s on true
+ order by g.last_seen desc
+ limit v_limit;
+end;
+$$;
+
+revoke all on function public.sun_dev_list_companies_v1768() from public,anon;
+revoke all on function public.sun_dev_delete_company_v1768(uuid,text) from public,anon;
+revoke all on function public.sun_dev_error_groups_v1768(integer,integer) from public,anon;
+
+grant execute on function public.sun_dev_list_companies_v1768() to authenticated;
+grant execute on function public.sun_dev_delete_company_v1768(uuid,text) to authenticated;
+grant execute on function public.sun_dev_error_groups_v1768(integer,integer) to authenticated;
diff --git a/package-lock.json b/package-lock.json
index aa5f61c..74793fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "caterium-app",
- "version": "17.6.7",
+ "version": "17.6.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "caterium-app",
- "version": "17.6.7",
+ "version": "17.6.8",
"devDependencies": {
"@playwright/test": "^1.51.0",
"http-server": "^14.1.1",
@@ -708,9 +708,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -728,9 +725,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -748,9 +742,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -768,9 +759,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -788,9 +776,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -808,9 +793,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -828,9 +810,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -848,9 +827,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -868,9 +844,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -894,9 +867,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -920,9 +890,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -946,9 +913,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -972,9 +936,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -998,9 +959,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1024,9 +982,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1050,9 +1005,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index 5caf603..690cf79 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"name": "caterium-app",
"private": true,
- "version": "17.6.7",
+ "version": "17.6.8",
"type": "module",
"scripts": {
- "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js",
+ "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js",
"test:static": "node tests/static-security.mjs",
"check:release": "node tests/release-check.mjs",
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
diff --git a/public/core/developer-console-v1768.js b/public/core/developer-console-v1768.js
new file mode 100644
index 0000000..ccb0c74
--- /dev/null
+++ b/public/core/developer-console-v1768.js
@@ -0,0 +1,152 @@
+(()=>{
+ '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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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=`
`;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=`Поиск Создать компанию
№ Компания Владелец Тариф Доступ Сотрудники Создана Действия
`;
+ 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=>`№${Number(r.company_number||0)} ${esc(r.workspace_name||'Компания')}
${esc(r.workspace_id)}
${esc(r.owner_email||r.pending_owner_email||'—')}${r.pending_owner_email?'ожидает регистрации
':''} ${esc(PLANS[r.plan_id]||r.plan_name||r.plan_id||'—')}до ${fmtDay(r.trial_ends_at||r.current_period_end)}
${esc(r.access_mode||r.status||'—')} ${Number(r.member_count||0)} / ${r.max_members==null?'∞':Number(r.max_members)} ${fmtDay(r.created_at)} Поддержка Диагностика Тариф ${r.access_mode==='blocked'?'Разблокировать':'Заблокировать'} Удалить
`).join('')||'Компаний не найдено. ';
+ 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('Создать компанию',`Название Email владельца ТарифБазовый Профессиональный Полный Срок, дней
Номер присвоится автоматически и больше не изменится, даже если другие компании будут удалены.
Отмена Создать
`);
+ $('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}`,`ТарифБазовый Профессиональный Полный Продлить на, дней30 90 365 Отмена Применить
`);$('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}`,`Заказы ${Number(d.orders||0)}
Клиенты ${Number(d.clients||0)}
Каталог ${Number(d.catalog_items||0)}
Ошибки 24 ч. ${Number(d.errors_24h||0)}
Revision: ${Number(d.revision||0)}
Обновлено: ${fmtDate(d.state_updated_at)}
Последняя копия: ${fmtDate(d.latest_backup)}
Режим доступа: ${esc(d.access_mode||'—')}
`)}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=`← Компании ${selected.company_number?`
№${Number(selected.company_number)} `:''}
${esc(selected.workspace_name)} ${selected.users.length} ${selected.users.length===1?'аккаунт':'аккаунтов'}
Аккаунт Роль в компании Регистрация Последний вход Статус ${selected.users.map(u=>{const memberships=u.workspaces.filter(w=>selected.workspace_id==='__orphan'||String(w.id)===String(selected.workspace_id));return `${esc(u.email||'—')} ${u.is_platform_admin?'Разработчик
':''}${esc(u.id)}
${memberships.length?memberships.map(w=>`${esc(w.role||'—')} ${w.display_name?`${esc(w.display_name)}
`:''}`).join(''):'—'} ${fmtDate(u.created_at)} ${fmtDate(u.last_sign_in_at)} ${memberships.some(w=>w.is_active===false)?'неактивен ':'активен '} `}).join('')||'В этой компании аккаунтов пока нет. '}
`;
+ $('sunDevAccountsBackV1768').onclick=()=>{selectedAccountsWorkspace=null;renderEnhancedTab('accounts')};return;
+ }
+ root.innerHTML=`Поиск компании ${users.length} аккаунтов всего
`;
+ 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=>`№${Number(g.company_number||0)} ${esc(g.workspace_name)}${esc(g.owner_email||'Владелец не указан')}
${g.users.length}
${g.users.length===1?'созданный аккаунт':'созданных аккаунтов'}
`).join('')+(orphan.length?`Без компании Системные или ещё не привязанные аккаунты
${orphan.length}
аккаунтов
`:'');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=`${plans.map(p=>`
`).join('')}
Матрица компактная: строка — функция, столбец — тариф. Изменения чекбоксов сохраняются сразу.
`;
+ 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.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=`Актуальные типы ошибок · 7 дней ${errors.length}
Предупреждения · 7 дней ${warns.length}
События этих типов ${total}
${prod.length?`Актуальные ошибки приложения Повторы сгруппированы. Старые локальные file:// копии больше не попадают в этот журнал.
Последний раз Компания Уровень Кол-во Версия Сообщение ${prod.map(g=>`${fmtDate(g.last_seen)} ${esc(g.workspace_name||'—')} ${esc(g.level||'error')} ${Number(g.occurrences||0)} ${esc(g.app_version||'—')} ${esc(g.message||'')} ${g.sample_stack?`Стек ${esc(g.sample_stack)} `:''} `).join('')}
`:`Актуальных ошибок нет За последние 7 дней production-журнал чист. Старые ошибки от локальных копий отфильтрованы на сервере.
`}Действия разработчика · ${(activity||[]).length} Дата Действие Компания / аккаунт Детали ${(activity||[]).map(a=>`${fmtDate(a.created_at)} ${esc(a.action)} ${esc(a.actor_email||'')}
${esc(a.workspace_name||a.target_email||'—')} ${esc(JSON.stringify(a.details||{}))} `).join('')||'Записей пока нет. '}
`;
+ }
+
+ async function renderEnhancedTab(tab){
+ if(!ENHANCED_TABS.has(tab))return false;const root=body();if(!root)return false;setTabChrome(tab);root.innerHTML='Загрузка…
';
+ 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=`Не удалось загрузить раздел ${esc(e?.message||e)}
`;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();
+})();
diff --git a/public/core/performance.js b/public/core/performance.js
index 93fbfe3..4379596 100644
--- a/public/core/performance.js
+++ b/public/core/performance.js
@@ -1,7 +1,7 @@
(()=>{
'use strict';
- const VERSION='17.6.7';
- const RELEASE='20260908-v17-6-7-classic-offer-pdf';
+ const VERSION='17.6.8';
+ const RELEASE='20260908-v17-6-8-developer-console-ux';
const critical=img=>img.closest('header,.brand,#sunCloudAuthGate,.sun-auth-gate')||img.id==='sunLoginLogo'||img.classList.contains('sun-live-catalog-logo');
const tune=img=>{
if(!(img instanceof HTMLImageElement)||critical(img))return;
@@ -103,14 +103,18 @@
if(window.SunUXFixV1764||document.getElementById('sunUXFixV1764Script'))return;
const script=document.createElement('script');script.id='sunUXFixV1764Script';script.src=`core/ux-fixes-v1764.js?v=${RELEASE}`;script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль ux-fixes-v1764.js');document.head.appendChild(script);
}
+ function loadDeveloperUX(){
+ if(window.SunDeveloperUXV1768||document.getElementById('sunDeveloperUXV1768Script'))return;
+ const script=document.createElement('script');script.id='sunDeveloperUXV1768Script';script.src=`core/developer-console-v1768.js?v=${RELEASE}`;script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль developer-console-v1768.js');document.head.appendChild(script);
+ }
const start=()=>{
- loadHotfix();loadOpsUX();loadUXFix();scan(document);startMemoryTimer();
+ loadHotfix();loadOpsUX();loadUXFix();loadDeveloperUX();scan(document);startMemoryTimer();
const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)queueImageScan(n)}));});
mo.observe(document.documentElement,{childList:true,subtree:true});
document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))scheduleMemoryRefresh(100,true)},true);
window.addEventListener('sun:cloud-state-applied',()=>scheduleMemoryRefresh(180,true));
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&developerVisible())scheduleMemoryRefresh(50,false)});
- window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadHotfix,loadOpsUX,loadUXFix,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}};
+ window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadHotfix,loadOpsUX,loadUXFix,loadDeveloperUX,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}};
};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
})();
diff --git a/public/index.html b/public/index.html
index 71f28a1..7d34a96 100644
--- a/public/index.html
+++ b/public/index.html
@@ -71,7 +71,7 @@
button{touch-action:manipulation}
#sunGlobalSearchBtn kbd{display:none!important}
#sunSyncSettingsCard>div[style*="grid-template-columns"]{grid-template-columns:1fr!important}
-}☼ Солнце Кейтеринг
Новый заказ Заказы Склад Статистика Заказы № Мероприятие Дата и время Адрес Сумма Статус Действия
Нажмите «Изменить», чтобы открыть заказ. Сумма, предоплата и остаток сохраняются вместе с заказом.
Склад В этой версии каталог продуктов пополняется из составов боксов. Остатки и цены добавим следующим шагом.
-
+
diff --git a/public/service-worker.js b/public/service-worker.js
index a3f8cf1..6a5d96d 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,8 +1,8 @@
-const CACHE='sun-catering-pwa-v72-20260908-v17-6-7-classic-offer-pdf';
-const VERSION='20260908-v17-6-7-classic-offer-pdf';
+const CACHE='sun-catering-pwa-v73-20260908-v17-6-8-developer-console-ux';
+const VERSION='20260908-v17-6-8-developer-console-ux';
const CORE=[
'./','./index.html',
- `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
+ `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
'./sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
diff --git a/tests/app.spec.mjs b/tests/app.spec.mjs
index 9dfd6e1..8b61aff 100644
--- a/tests/app.spec.mjs
+++ b/tests/app.spec.mjs
@@ -156,7 +156,7 @@ test('Developer Console memory refresh is bounded and does not react to its own
const result=await page.evaluate(()=>({count:window.__rpcCount,box:document.querySelectorAll('#sunDevMemoryV1761').length,version:document.getElementById('sunDevReleaseVersion')?.textContent}));
expect(result.count).toBeLessThanOrEqual(2);
expect(result.box).toBe(1);
- expect(result.version).toBe('17.6.7');
+ expect(result.version).toBe('17.6.8');
});
test('mobile body does not overflow viewport', async ({ page }, testInfo) => {
test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
@@ -224,3 +224,19 @@ test('v17.6.7 classic client PDF renders distinctive cover and keeps preview/dow
});
expect(result.count).toBe(2);expect(result.id).toBe('event-ticket');expect(result.cover).toBe('1');expect(result.classic).toBe(10);expect(result.archive).toBe(3);
});
+
+
+test('v17.6.8 Developer Console UX module boots', async ({ page }) => {
+ await page.goto('/index.html',{waitUntil:'domcontentloaded'});
+ const source=fs.readFileSync(path.join(process.cwd(),'public','core','developer-console-v1768.js'),'utf8');
+ await page.evaluate(()=>{
+ document.body.innerHTML='Обзор Компании Аккаунты Тарифы Журнал
';
+ window.SunDeveloperV22={open:async()=>true,activateTab:async()=>true};
+ window.SunCloudV2={getClient:()=>null};
+ window.SunEnterprise={toast:()=>null};
+ });
+ await page.addScriptTag({content:source});
+ await page.waitForFunction(()=>Boolean(window.SunDeveloperUXV1768),null,{timeout:5000});
+ const result=await page.evaluate(()=>window.SunDeveloperUXV1768.checks());
+ expect(result.version).toBe('17.6.8');
+});
diff --git a/tests/release-check.mjs b/tests/release-check.mjs
index 68c0d95..df10a38 100644
--- a/tests/release-check.mjs
+++ b/tests/release-check.mjs
@@ -4,7 +4,7 @@ const root=process.cwd(), pub=path.join(root,'public');
const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
-const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js'),classic=read('core/classic-offer-pdf-v1767.js');
+const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js'),classic=read('core/classic-offer-pdf-v1767.js'),developerUX=read('core/developer-console-v1768.js');
const pkg=JSON.parse(readRoot('package.json'));
const lock=JSON.parse(readRoot('package-lock.json'));
const releaseManifest=JSON.parse(readRoot('docs/release-manifest.json'));
@@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
-check(sw.includes('v72-20260908-v17-6-7-classic-offer-pdf')&&sw.includes('classic-offer-pdf-v1767.js'),'service worker cache is v17.6.7');
-check(index.includes('20260908-v17-6-7-classic-offer-pdf')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.7');
+check(sw.includes('v73-20260908-v17-6-8-developer-console-ux')&&sw.includes('classic-offer-pdf-v1767.js')&&sw.includes('developer-console-v1768.js'),'service worker cache is v17.6.8');
+check(index.includes('20260908-v17-6-8-developer-console-ux')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.8');
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
@@ -39,8 +39,8 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
check(releaseManifest.channel==='production','release manifest channel is production');
-check(String(releaseManifest.pwaCache||'').includes('v72-20260908-v17-6-7-classic-offer-pdf'),'release manifest points to current PWA cache');
-check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.6.7');
+check(String(releaseManifest.pwaCache||'').includes('v73-20260908-v17-6-8-developer-console-ux'),'release manifest points to current PWA cache');
+check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.6.8');
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
check(runtime.includes("const VERSION = '17.6.5'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
check(runtime.includes('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh');
@@ -49,11 +49,15 @@ check(performance.includes('pendingImageRoots')&&performance.includes('queueImag
check(runtime.includes('explicitTemplate')&&runtime.includes("OFFER_TEMPLATE_IDS.has(explicitTemplate)"),'per-order proposal template survives render and PDF');
check(runtime.includes("else if(id==='midnight-glass')")&&runtime.includes("else if(id==='emerald-gold')")&&runtime.includes("if(id==='editorial-grid')"),'existing proposal layout sequences remain available');
check(runtime.includes('data-template-mini')&&ux.includes('data-template-mini'),'global and per-offer selectors show structural PDF previews');
-check(pkg.version==='17.6.7','package version is v17.6.7');
+check(pkg.version==='17.6.8','package version is v17.6.8');
check(classic.includes("const VERSION='17.6.7'")&&classic.includes('CLASSIC_IDS')&&classic.includes('ARCHIVE_IDS'),'classic proposal PDF module is v17.6.7');
check(runtime.includes("'warm-sun'")&&runtime.includes("'bento-cards'")&&runtime.includes("'event-story'")&&runtime.includes("'personal-letter'")&&runtime.includes("'event-ticket'")&&runtime.includes("'solar-experience'"),'10 classic proposal designs are available');
check(runtime.includes("'midnight-compact'")&&runtime.includes("'black-gold'")&&runtime.includes("'neon-emerald'"),'hidden archive proposal templates are restored');
check(runtime.includes('SunClassicOfferPDFV1767')&&runtime.includes('renderOfferPdfPagesBase'),'classic renderer wraps the same PDF pages used by preview/download');
check(classic.includes('data-sun-classic-cover')&&classic.includes('renderPages'),'classic renderer adds real A4 cover pages');
check(releaseManifest.offerTemplates===13&&releaseManifest.classicOfferTemplates===10,'release manifest exposes 10 classic + 3 archive templates');
+check(developerUX.includes("const VERSION='17.6.8'")&&developerUX.includes('sun_dev_delete_company_v1768')&&developerUX.includes('sun_dev_error_groups_v1768'),'Developer Console v17.6.8 module is versioned');
+check(developerUX.includes('selectedAccountsWorkspace')&&developerUX.includes('sun-dev-plan-matrix'),'accounts are grouped by company and plans use compact matrix');
+check(developerUX.includes('KNOWN_DOM_RACE')&&developerUX.includes('prepareAsyncRoots'),'Developer Console async DOM race is guarded');
+check(releaseManifest.developerCompanyNumbers===true&&releaseManifest.developerCompanyDelete===true&&releaseManifest.developerAccountsGroupedByCompany===true&&releaseManifest.developerPlansCompactMatrix===true&&releaseManifest.developerJournalGroupedErrors===true,'release manifest records Developer Console UX changes');
if(bad)process.exit(1);
diff --git a/tests/static-security.mjs b/tests/static-security.mjs
index 50fb9b2..5ebb0a4 100644
--- a/tests/static-security.mjs
+++ b/tests/static-security.mjs
@@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8');
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
const ok=m=>console.log('OK:',m);
-const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js');
+const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js'),developerUX=readPub('core/developer-console-v1768.js');
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded');
if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed');
for(const raw of ['${b.name} ','${o.event} ','${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`);
@@ -27,8 +27,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
-if(!sw.includes('v17-6-7-classic-offer-pdf')||!sw.includes('classic-offer-pdf-v1767.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.7');
-if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-7-classic-offer-pdf')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
+if(!sw.includes('v17-6-8-developer-console-ux')||!sw.includes('developer-console-v1768.js')||!sw.includes('classic-offer-pdf-v1767.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.8');
+if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-8-developer-console-ux')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
@@ -44,5 +44,6 @@ for(const marker of ['CLOUD_RPC_TIMEOUT_MS=12000','CLOUD_CONFLICT_MAX_RETRIES=4'
if(!hotfix.includes('checkPlatformAdmin?.(false)')||!hotfix.includes('},10000);'))fail('Developer fallback polling is still aggressive');else ok('Developer fallback polling is throttled');
if(!ux.includes('sunMenuIconV1766')||!ux.includes('sun-offer-template-mini-editorial-grid')||!runtime.includes('explicitTemplate'))fail('v17.6.6 proposal/menu markers missing');else ok('v17.6.6 proposal/menu markers present');
if(!classic.includes("const VERSION='17.6.7'")||!classic.includes('CLASSIC_IDS')||!classic.includes('ARCHIVE_IDS')||!classic.includes('renderPages'))fail('v17.6.7 classic PDF module missing');else ok('v17.6.7 classic PDF module present');
-if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux+classic)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux+classic))fail('possible secret in ops/hotfix/ux/classic module');else ok('ops/hotfix/ux/classic modules have no hard-coded secret');
+if(!developerUX.includes("const VERSION='17.6.8'")||!developerUX.includes('sun_dev_delete_company_v1768')||!developerUX.includes('sun_dev_error_groups_v1768')||!developerUX.includes('selectedAccountsWorkspace')||!developerUX.includes('sun-dev-plan-matrix')||!developerUX.includes('KNOWN_DOM_RACE'))fail('v17.6.8 Developer Console UX module missing');else ok('v17.6.8 Developer Console UX module present');
+if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux+classic+developerUX)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux+classic+developerUX))fail('possible secret in ops/hotfix/ux/classic/developer module');else ok('ops/hotfix/ux/classic/developer modules have no hard-coded secret');
if(process.exitCode)process.exit(process.exitCode);
\ No newline at end of file