Интерфейс клиента без технических подробностей; дубли убраны

Техника — только в режиме разработчика (devOn):
- версии: клиенту — bj, шлюз НРД, СКЗИ, QUIK, DEPO, без «Подробностей»
  (Java, bj-crypto, пути, установщик, ОС, PostgreSQL — разработчикам)
- Криптография: строки crypto-service/java, параметры провайдера и сокет,
  серийные номера, журнал сверки (клиенту — только при ошибке), дамп справочника
  и путь nsd-certs; убрана карточка «Ключ организации» — дубль сводки
- НРД: строка версии ИШ (дубль сводки), «Расширенные» (адреса, контейнеры,
  config.xml), имя профиля ключа, адрес канала; контур по-русски
- Обновления: имена файлов и служб, адрес артефактории и ключ издателя
- Диагностика: имена служб и сырые состояния; справочник участников — без кодов
- ключи ролей и модулей лицензии, путь users.json, DSN/Ping/in-memory, FTV_M2M,
  путь Валидаты и служба bj-crypto в мастере, «проверена ZETIT», Samba

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-21 23:44:52 +03:00
parent 29e19c78a9
commit f1065e369a
9 changed files with 126 additions and 77 deletions
@@ -45,14 +45,14 @@
</div>
<div class="card" id="versions">
<h2>Версии систем</h2>
<p class="muted">Что установлено на этом сервере и с чем работает bj: собственные компоненты и внешние системы. Та же версия показана в разделе каждого модуля. Сведения на {{msk .Versions.At}}.</p>
<p class="muted">bj и системы, с которыми он работает. Сведения на {{msk .Versions.At}}.</p>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Система</th><th>Версия</th><th>Подробности</th></tr></thead>
<tbody>{{range .Versions.Items}}<tr>
<td>{{if and .Section (ne .Section "overview")}}<a href="#{{.Section}}" onclick="var b=document.querySelector('.settings-nav [data-sec=&quot;{{.Section}}&quot;]');if(b){b.click();return false}">{{.Title}}</a>{{else}}{{.Title}}{{end}}</td>
<thead><tr><th>Система</th><th>Версия</th>{{if devOn}}<th>Подробности</th>{{end}}</tr></thead>
<tbody>{{range .Versions.Items}}{{if or devOn (clientVer .Key)}}<tr>
<td>{{if and .Section (ne .Section "overview")}}<a href="#{{.Section}}" onclick="var b=document.querySelector('.settings-nav [data-sec=&quot;{{.Section}}&quot;]');if(b){b.click();return false}">{{verTitle .}}</a>{{else}}{{verTitle .}}{{end}}</td>
<td>{{if .Version}}<strong>{{.Version}}</strong>{{else}}<span class="muted">не определена</span>{{end}}</td>
<td class="muted" style="font-size:12.5px">{{.Detail}}{{with .Err}} <span style="color:var(--err)">{{.}}</span>{{end}}</td>
</tr>{{end}}</tbody>
{{if devOn}}<td class="muted" style="font-size:12.5px">{{.Detail}}{{with .Err}} <span style="color:var(--err)">{{.}}</span>{{end}}</td>{{end}}
</tr>{{end}}{{end}}</tbody>
</table></div>
<form method="post" action="/admin/setup/versions/refresh" style="margin-top:10px"><input type="hidden" name="section" value="versions"><button type="submit" class="btn btn-secondary">Обновить сведения</button></form>
</div>
@@ -64,7 +64,7 @@
{{template "verline" (.Versions.Get "postgres")}}
<div class="card">
<h2><span class="dot {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span>PostgreSQL</h2>
<p class="muted">Журнал сделок m2m-core и принимающая БД. Сейчас: {{if .Settings.Postgres.DSN}}<code>подключено</code>{{else}}<code>in-memory</code> (данные не сохраняются){{end}}.</p>
<p class="muted">Здесь bj хранит переводы, журнал и настройки. Сейчас: {{if .Settings.Postgres.DSN}}<b style="color:var(--ok)">подключена</b>{{else}}<b style="color:var(--err)">не подключена</b> — данные не сохраняются{{end}}.</p>
{{if and devOn (not .Settings.Postgres.DSN)}}
<div style="background:var(--card-2);border:1px solid var(--accent);border-radius:10px;padding:16px;margin-top:12px">
<h3 style="margin:0 0 8px">Подключить автоматически</h3>
@@ -75,11 +75,11 @@
</div>
{{end}}
<details {{if not .Settings.Postgres.DSN}}open{{end}} style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">{{if .Settings.Postgres.DSN}}Изменить подключение{{else}}…или ввести DSN вручную{{end}}</summary>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">{{if .Settings.Postgres.DSN}}Изменить подключение{{else}}…или ввести строку подключения вручную{{end}}</summary>
<form method="post" action="/admin/setup/postgres" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>DSN</label>
<input type="text" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable">
<p class="muted" style="margin:0">При сохранении выполняется Ping.</p>
<label for="db-dsn">Строка подключения к базе</label>
<input type="text" id="db-dsn" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://пользователь:пароль@адрес:5432/bj?sslmode=disable">
<p class="muted" style="margin:0">При сохранении bj проверит подключение.</p>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</details>
@@ -170,7 +170,7 @@
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">роли и права, домен вручную: контроллер, сертификат, группы, перепроверка, время сессии</span></summary>
<h3 class="adv-h">Роли и права</h3>
<table style="font-size:12.5px"><thead><tr><th>Роль</th><th>Что разрешено</th></tr></thead><tbody>
{{range $U.Roles}}<tr><td style="white-space:nowrap"><b>{{.Title}}</b><br><code style="font-size:11px">{{.Key}}</code></td><td>{{.Desc}}</td></tr>{{end}}
{{range $U.Roles}}<tr><td style="white-space:nowrap"><b>{{.Title}}</b>{{if devOn}}<br><code style="font-size:11px">{{.Key}}</code>{{end}}</td><td>{{.Desc}}</td></tr>{{end}}
{{range $U.Hidden}}<tr class="muted"><td style="white-space:nowrap">{{.Title}}</td><td>Недоступна: в лицензии нет модуля «{{.Missing}}».</td></tr>{{end}}
</tbody></table>
<p class="muted" style="font-size:12px;margin-top:6px">{{if $U.Modules}}Подключённые модули ({{range $i, $m := $U.Modules}}{{if $i}}, {{end}}«{{$m.Title}}»{{end}}) можно выдать любому пользователю дополнительно к роли. {{end}}Роли и модули показываются по лицензии: права модуля, которого нет в лицензии, не действуют. Настройки и пользователи — только администратор.</p>
@@ -219,7 +219,7 @@
<form method="post" action="/admin/setup/auth/recheck"><button class="btn btn-secondary" type="submit">Перепроверить сейчас</button></form>
{{else}}<span class="muted"><span class="dot warn"></span>Перепроверка групп выключена: роль доменного пользователя меняется только при следующем входе. Укажите сервисную учётную запись выше.</span>{{end}}
</div>
<p class="muted" style="font-size:12.5px;margin-top:14px">Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>
{{if devOn}}<p class="muted" style="font-size:12.5px;margin-top:14px">Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>{{else}}<p class="muted" style="font-size:12.5px;margin-top:14px">Если потерян доступ администратора — обратитесь в поддержку ZETIT.</p>{{end}}
</details>
</div>
</section>
@@ -252,18 +252,7 @@
<div class="card">
<details class="adv-box" {{if not (and cryptoReady orgKeyCert)}}open{{end}}>
<summary><span class="sum-h">Перенастроить</span><span class="muted">ключ организации, проверка СКЗИ, сертификаты НРД в справочнике шлюза, версии</span></summary>
<div style="margin-top:16px">{{template "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "validata")}}{{template "verline" (.Versions.Get "java")}}</div>
{{$kc := orgKeyCert}}
{{if and cryptoReady $kc}}
<div class="card" id="sec-orgkey">
<h2><span class="dot {{if lt (index $kc "Days") 30}}warn{{else}}ok{{end}}"></span>Ключ организации</h2>
<p style="margin:6px 0">Сертификат «{{index $kc "Owner"}}», выдан: {{index $kc "Issuer"}} — действует до <b>{{index $kc "Until"}}</b> (осталось {{index $kc "Days"}} дн.).</p>
{{if lt (index $kc "Days") 30}}<p style="color:var(--warn);margin:6px 0">⚠ Сертификат скоро закончится. Получите новый ключ в Удостоверяющем центре Московской биржи и установите его — без этого обмен с НРД остановится.</p>
<a href="/admin/setup/keywizard" class="btn btn-ok" style="margin-top:6px;display:inline-block">Установить новый ключ →</a>
{{else}}<p class="muted" style="font-size:13px;margin:6px 0 0">Перевыпустили ключ (новый сертификат) или нужен ключ другого контура — <a href="/admin/setup/keywizard">заменить ключ</a>: bj поможет подключить флешку или записать новый ключ из архива.</p>{{end}}
</div>
{{end}}
{{if devOn}}<div style="margin-top:16px">{{template "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "java")}}</div>{{end}}
<div class="card">
<h2><span class="dot {{if cryptoReady}}ok{{else}}warn{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
@@ -276,7 +265,7 @@
<form method="post" action="/admin/setup/restart-server" style="margin:0" onsubmit="return confirm('Перезапустить bj-server? Через 5-10 сек страница вернётся.');"><button type="submit" class="btn btn-secondary">↻ bj-server</button></form>
{{end}}
</div>
<details style="margin-top:14px">
{{if devOn}}<details style="margin-top:14px">
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Параметры провайдера (для совместимости)</summary>
<form method="post" action="/admin/setup/crypto" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
<label>Провайдер</label>
@@ -290,18 +279,18 @@
<input type="text" name="socket_path" value="{{.Settings.Crypto.SocketPath}}" placeholder="/run/bj/crypto.sock">
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</details>{{end}}
</div>
{{/* Доверенные сертификаты НРД в справочнике ИШ */}}
<div class="card" id="sec-nsdcerts">
<h2><span class="dot {{if .NSDCerts.Pending}}warn{{else if .NSDCerts.LastRun.IsZero}}warn{{else}}ok{{end}}"></span>Сертификаты НРД в справочнике ИШ</h2>
<p class="muted">Комплект доверенных сертификатов крипто-сервера НРД поставляется вместе с bj и при обновлении приезжает всем клиентам. bj сам сверяет справочник активного профиля ИШ{{if .NSDCerts.Profile}} (<code>{{.NSDCerts.Profile}}</code>){{end}}: недостающие добавляет (только если их УЦ уже есть в справочнике — боевой сертификат не попадёт в тестовый профиль), старые удаляет строго после разрешённого НРД срока. Каждое изменение — с бэкапом, проверкой подписи и откатом при ошибке; ИШ перезапускается на несколько секунд.</p>
<h2><span class="dot {{if .NSDCerts.Pending}}warn{{else if .NSDCerts.LastRun.IsZero}}warn{{else}}ok{{end}}"></span>Сертификаты НРД в справочнике шлюза</h2>
<p class="muted">Сертификаты НРД приходят вместе с обновлениями bj. bj сам добавляет недостающие в справочник шлюза НРД и удаляет старые после срока, разрешённого НРД; перед каждым изменением — резервная копия.{{if devOn}}{{if .NSDCerts.Profile}} Профиль <code>{{.NSDCerts.Profile}}</code>.{{end}}{{end}}</p>
<table style="margin-top:12px">
<thead><tr><th>Сертификат</th><th>Контур</th><th>Действует</th><th>Статус</th></tr></thead>
<tbody>{{range .NSDCerts.Rows}}<tr>
<td><b>{{.Title}}</b><div class="muted" style="font-size:11px">{{.SubjectCN}} · выдан: {{.IssuerCN}} · серийный <code>{{.Serial}}</code></div></td>
<td>{{.Contour}}</td>
<td><b>{{.Title}}</b>{{if devOn}}<div class="muted" style="font-size:11px">{{.SubjectCN}} · выдан: {{.IssuerCN}} · серийный <code>{{.Serial}}</code></div>{{end}}</td>
<td>{{contourRu .Contour}}</td>
<td style="white-space:nowrap">{{.ValidFrom}} — {{.ValidTo}}</td>
<td>{{if eq .Kind "ok"}}<span style="color:var(--ok);font-weight:600">✓ {{.Status}}</span>{{else if eq .Kind "add"}}<span style="color:var(--warn);font-weight:600">⬆ {{.Status}}</span>{{else}}<span class="muted">{{.Status}}</span>{{end}}{{if .RemoveNote}}<div style="font-size:11.5px;margin-top:3px;{{if .RemoveSerial}}color:var(--warn);font-weight:600{{else}}color:var(--muted){{end}}">{{.RemoveNote}}</div>{{end}}</td>
</tr>{{end}}</tbody>
@@ -312,10 +301,10 @@
<form method="post" action="/admin/setup/nsdcerts" style="margin:0;display:flex;align-items:center;gap:8px"><input type="hidden" name="section" value="sec-nsdcerts"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin:0"><input type="checkbox" name="auto" {{if not .NSDCerts.Manual}}checked{{end}} style="width:auto" onchange="this.form.submit()"> автоматически (при старте и каждые 6 ч)</label></form>
{{if not .NSDCerts.LastRun.IsZero}}<span class="muted" style="font-size:12px">сверено {{.NSDCerts.LastRun.Format "02.01.2006 15:04"}}</span>{{end}}
</div>
{{if .NSDCerts.LastLog}}<pre style="margin-top:10px;font-size:11.5px;white-space:pre-wrap">{{.NSDCerts.LastLog}}</pre>{{end}}
{{if .NSDCerts.Store}}<details style="margin-top:8px"><summary class="muted" style="cursor:pointer;font-size:12px">Содержимое справочника профиля ({{len .NSDCerts.Store}} записей)</summary>
{{if and .NSDCerts.LastLog (or devOn (looksErr .NSDCerts.LastLog))}}<pre style="margin-top:10px;font-size:11.5px;white-space:pre-wrap">{{.NSDCerts.LastLog}}</pre>{{end}}
{{if and devOn .NSDCerts.Store}}<details style="margin-top:8px"><summary class="muted" style="cursor:pointer;font-size:12px">Содержимое справочника профиля ({{len .NSDCerts.Store}} записей)</summary>
<table style="margin-top:8px;font-size:12px"><thead><tr><th>Владелец</th><th>Издатель</th><th>Серийный</th><th>До</th><th>Хранилище</th></tr></thead><tbody>{{range .NSDCerts.Store}}<tr><td>{{if eq .Kind "crl"}}<span class="muted">CRL</span>{{else}}{{.SubjectCN}}{{if .SelfSigned}} <span class="muted">(УЦ)</span>{{end}}{{end}}</td><td>{{.IssuerCN}}</td><td><code>{{.Serial}}</code></td><td>{{.NotAfter}}{{if .CertNotAfter}}<div class="muted" style="font-size:11px" title="Срок закрытого ключа (PrivateKeyUsagePeriod): подписывать можно до {{.NotAfter}}. Срок самого сертификата — {{.CertNotAfter}}.">ключ; сертификат до {{.CertNotAfter}}</div>{{end}}</td><td>{{.Store}}</td></tr>{{end}}</tbody></table></details>{{end}}
<p class="muted" style="font-size:12px;margin-top:8px">Свои сертификаты можно положить в <code>/var/lib/bj/.bj/nsd-certs/*.cer</code> — они добавляются по тем же правилам и никогда не удаляются автоматически.</p>
{{if devOn}}<p class="muted" style="font-size:12px;margin-top:8px">Свои сертификаты можно положить в <code>/var/lib/bj/.bj/nsd-certs/*.cer</code> — они добавляются по тем же правилам и никогда не удаляются автоматически.</p>{{end}}
</div>
{{/* Сертификаты УЦ (старый способ: теперь сертификаты НРД приходят комплектом с bj) — только для разработки */}}
@@ -347,7 +336,6 @@
{{/* ============ НРД ============ */}}
<section class="settings-section" id="sec-nsd">
<h1>НРД</h1>
{{template "verline" (.Versions.Get "ish")}}
{{template "nsdStatus" .}}
<div class="card">
<details class="adv-box" {{if not (and (index orgEff "INN") (index orgEff "Code") .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID .Settings.NSD.IGWBaseURL)}}open{{end}}>
@@ -362,7 +350,7 @@
</div>
<div class="card" id="nsd-participant">
<h2>Регистрация участника M2M</h2>
<p class="muted">Анкета участника <code>M2MTransferParticipantForm</code> — НРД требует её перед работой: без регистрации сервис МОСТ отбивает запросы кодом <b>M2M14</b>. bj заполняет анкету по реквизитам фирмы (настройки НРД и справочник участников), даёт проверить и показывает XML, затем отправляет в боевой контур через ИШ (пакет <code>#M2MPF</code>) или письмом на <code>M2MOST@nsd.ru</code>.</p>
<p class="muted">НРД требует анкету участника перед началом работы: без регистрации НРД не принимает запросы на переводы. bj заполняет анкету по реквизитам организации, даёт проверить и отправляет в НРД через шлюз (или её можно отправить письмом на <code>M2MOST@nsd.ru</code>).</p>
<details id="pf-box">
<summary style="cursor:pointer;color:var(--accent)">Открыть анкету участника</summary>
<iframe id="pf-frame" title="Анкета участника M2M" style="width:100%;height:1200px;border:1px solid var(--border);border-radius:10px;margin-top:10px;background:#fff" loading="lazy"></iframe>
@@ -371,7 +359,7 @@
<script>(function(){var d=document.getElementById('pf-box'),f=document.getElementById('pf-frame');
if(d&&f) d.addEventListener('toggle',function(){ if(d.open&&!f.getAttribute('src')) f.setAttribute('src','/admin/setup/participant-form'); });})();</script>
</div>
<div class="card">
{{if devOn}}<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">шлюз вручную: контур, адрес веб-сервиса, ключевой контейнер, версия ИШ — обычно bj задаёт их сам</span></summary>
<p class="muted" style="margin-top:10px">{{if not .Settings.NSD.IGWBaseURL}}{{if devOn}}<code>mock-режим</code> — без реального ИШ.{{else}}ИШ не настроен — обмен с НРД не выполняется.{{end}}{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}}</p>
@@ -417,7 +405,7 @@
})();
</script>
</details>
</div>
</div>{{end}}
</details>
</div>
</section>
@@ -843,13 +831,13 @@
<h2><span class="dot {{$bad := 0}}{{range .Units}}{{if not .OK}}{{$bad = 1}}{{end}}{{end}}{{if eq $bad 1}}err{{else}}ok{{end}}"></span>Службы</h2>
<p class="muted">Ядро и модули работают отдельными службами: если служба модуля не запущена, его раздел в bj отвечает ошибкой связи. Здесь видно состояние и можно перезапустить службу, не заходя на сервер.</p>
<div class="tw" style="margin-top:8px"><table>
<thead><tr><th>Служба</th><th>Что делает</th><th>Состояние</th><th>С</th><th></th></tr></thead>
<thead><tr>{{if devOn}}<th>Служба</th>{{end}}<th>Что делает</th><th>Состояние</th><th>С</th><th></th></tr></thead>
<tbody>{{range .Units}}<tr>
<td style="white-space:nowrap"><code>{{.Unit}}</code></td>
{{if devOn}}<td style="white-space:nowrap"><code>{{.Unit}}</code></td>{{end}}
<td>{{.Title}}</td>
<td>{{if .OK}}<span class="badge ok">работает</span>{{else}}<span class="badge err">{{.State}}{{with .Sub}} / {{.}}{{end}}{{with .Result}} ({{.}}){{end}}</span>{{end}}</td>
<td>{{if .OK}}<span class="badge ok">работает</span>{{else}}<span class="badge err">не работает{{if devOn}}: {{.State}}{{with .Sub}} / {{.}}{{end}}{{with .Result}} ({{.}}){{end}}{{end}}</span>{{end}}</td>
<td class="muted">{{if .Since}}{{.Since}}{{else}}—{{end}}</td>
<td style="white-space:nowrap"><form method="post" action="/admin/setup/units/restart" style="margin:0" onsubmit="return confirm('{{if .Core}}Перезапустить ядро? Страница настроек на несколько секунд станет недоступна.{{else}}Перезапустить службу {{.Unit}}?{{end}}');">
<td style="white-space:nowrap"><form method="post" action="/admin/setup/units/restart" style="margin:0" onsubmit="return confirm('{{if .Core}}Перезапустить bj? Страница настроек на несколько секунд станет недоступна.{{else}}Перезапустить «{{.Title}}»?{{end}}');">
<input type="hidden" name="section" value="sec-units"><input type="hidden" name="unit" value="{{.Unit}}">
<button type="submit" class="btn btn-secondary">Перезапустить</button></form></td>
</tr>{{end}}</tbody></table></div>
@@ -900,9 +888,9 @@
<div class="card">
<h2>Справочник участников M2M</h2>
<p class="muted">Запрос актуального справочника участников сервиса (пакет <code>#M2MHR</code> в НРД). В ответ НРД присылает <code>#M2MTH</code> — карту <strong>ИНН → MC-код</strong> депозитариев/брокеров и перечень мест расчётов. Нужно для сопоставления ИНН контрагента с кодом участника (блокеры вида M2M14/M2M15) и понимания, кто вообще зарегистрирован.</p>
<p class="muted">Справочник участников M2M из НРД: по нему bj узнаёт код участника по ИНН контрагента. bj запрашивает его сам; кнопка — запросить сейчас.</p>
<form method="post" action="/admin/setup/handbook-request" style="margin-top:12px"><button type="submit" class="btn">Запросить справочник у НРД</button></form>
<p class="muted" style="font-size:12px;margin-top:8px">Ответ придёт во входящие ИШ (~30-60с). Список появится в логе bj-server и в <code>m2m-handbook.json</code>.</p>
<p class="muted" style="font-size:12px;margin-top:8px">Ответ НРД приходит через 30–60 секунд.</p>
</div>
{{if devOn}} <div class="card">
@@ -1064,7 +1052,7 @@
{{if not .License.ExpiresAt.IsZero}}<tr><td class="muted">Ключ действует до</td><td>{{.License.ExpiresAt.Format "02.01.2006"}} {{if .License.Valid}}<span class="muted">(осталось {{.License.DaysLeft}} дн.)</span>{{end}}</td></tr>{{end}}
{{if not .License.ContractUntil.IsZero}}<tr><td class="muted">Договор до</td><td>{{.License.ContractUntil.Format "02.01.2006"}}</td></tr>{{end}}
<tr><td class="muted">Обновления</td><td>{{if .License.AllowsUpdates}}<span style="color:var(--ok)">включены</span>{{else}}<span class="muted">не входят в план</span>{{end}}</td></tr>
<tr><td class="muted" style="vertical-align:top">Модули</td><td>{{range .Features}}<div style="display:flex;gap:8px;align-items:center;margin:2px 0"><span class="dot {{if .On}}ok{{else}}err{{end}}"></span>{{.Title}} <code style="font-size:11px;color:var(--muted)">{{.Key}}</code>{{if not .On}}<span class="muted" style="font-size:11px">— не входит в лицензию</span>{{end}}</div>{{end}}</td></tr>
<tr><td class="muted" style="vertical-align:top">Модули</td><td>{{range .Features}}<div style="display:flex;gap:8px;align-items:center;margin:2px 0"><span class="dot {{if .On}}ok{{else}}err{{end}}"></span>{{.Title}}{{if devOn}} <code style="font-size:11px;color:var(--muted)">{{.Key}}</code>{{end}}{{if not .On}}<span class="muted" style="font-size:11px">— не входит в лицензию</span>{{end}}</div>{{end}}</td></tr>
<tr><td class="muted">Статус</td><td>{{.License.Message}}</td></tr>
</tbody>
</table>