Files
Bridge-and-Join-s/modules/npr/web/list.html
T
zuevav 5527a464ca НПР / УДС — отдельная служба bj-npr
Модуль вынесен в modules/npr + cmd/bj-npr (127.0.0.1:8083), обновляется
независимо от ядра. Служба читает подключение из setup.json и принимает
запросы только со служебным ключом модулей (/var/lib/bj/.bj/module.key,
создаёт bj-server). Полные страницы отдаются фрагментом (X-BJ-Layout: page).

bj-server хранит настройки и проверку подключения, проксирует /admin/npr
после входа, прав и лицензии и встраивает страницы модуля в общий макет
(admin.modulePage); если служба не отвечает — понятная страница.
Unit bj-npr.service встроен в установщик, установщик включает службы
установленных модулей; build-release.sh собирает bj-npr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:00:31 +03:00

33 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "npr_list"}}
{{template "npr_css"}}
<script src="/admin/npr/static/htmx.min.js"></script>
<div class="doc-header" style="margin-bottom:12px"><h1>📊 Риск-монитор: НПР и УДС клиентов</h1><p class="lede">НПР1 / НПР2 / УДС клиентов маржинальной торговли по данным экспорта QUIK. Модуль перенесён как есть, доработки — по мере необходимости.</p></div>
<nav class="ntabs">
<a href="?tab=margin" class="{{if eq .Tab "margin"}}active{{end}}">Маржинальный риск <span class="count" id="tab_margin_count">—</span></a>
<a href="?tab=debt" class="{{if eq .Tab "debt"}}active{{end}}">Дебиторская задолженность <span class="count" id="tab_debt_count">—</span></a>
</nav>
{{if eq .Tab "margin"}}<p class="tab-desc">Клиенты с открытыми маржинальными позициями (есть начальная или минимальная маржа). У кого УДС близок к 1 — под маржин-колл, у кого &lt;0 — под принудительное закрытие.</p>
{{else}}<p class="tab-desc">Клиенты без открытых позиций, но с отрицательной стоимостью портфеля (должны брокеру). УДС у таких счетов в QUIK уходит в −9,99 — это задолженность, а не маржин-колл.</p>{{end}}
<div class="nhelp"><strong>УДС</strong> — уровень достаточности средств (чем меньше, тем опаснее). <strong>НПР1</strong> — запас относительно начальной маржи (отрицательный — запрещено наращивать позиции). <strong>НПР2</strong> — запас относительно минимальной маржи (отрицательный — начинается принудительное закрытие).
<div class="legend"><span class="legend-item"><span class="legend-box" style="background:#fee2e2"></span> маржин-колл (УДС&lt;1)</span><span class="legend-item"><span class="legend-box" style="background:#fef3c7"></span> внимание (УДС&lt;1,5)</span><span class="legend-item"><span class="legend-box" style="background:#d1fae5"></span> «∞» избыточно (УДС≥9,99)</span></div></div>
<section class="nsummary" hx-get="/admin/npr/api/summary?tab={{.Tab}}" hx-trigger="load, every 60s" hx-swap="none" hx-on::after-request="try{const d=JSON.parse(event.detail.xhr.responseText);for(const [k,v] of Object.entries({sum_total:d.total,sum_calls:d.calls,sum_warn:d.warn,sum_ok:d.ok,sum_inf:d.inf,tab_margin_count:d.tab_margin,tab_debt_count:d.tab_debt})){const e=document.getElementById(k);if(e)e.textContent=v;}}catch(e){}">
<article><header>всего клиентов</header><strong id="sum_total">—</strong></article>
<article class="tile-call"><header>маржин-колл (УДС&lt;1)</header><strong id="sum_calls">—</strong></article>
<article class="tile-warn"><header>внимание (УДС&lt;1,5)</header><strong id="sum_warn">—</strong></article>
<article class="tile-ok"><header>норма</header><strong id="sum_ok">—</strong></article>
<article class="tile-ok"><header>избыточно (∞)</header><strong id="sum_inf">—</strong></article>
</section>
<div class="nfresh" hx-get="/admin/npr/api/freshness" hx-trigger="load, every 15s" hx-swap="none" hx-on::after-request="try{const d=JSON.parse(event.detail.xhr.responseText);const now=d.server_now?new Date(d.server_now):new Date();const tick=d.last_tick?new Date(d.last_tick):null;const ago=tick?Math.round((now-tick.getTime())/1000):null;const ok=ago!==null&&ago<90;document.getElementById('fresh_dot').style.background=ok?'#10b981':'#dc2626';document.getElementById('fresh_text').textContent=tick?('Последнее обновление из QUIK: '+tick.toLocaleString('ru-RU')+' ('+ago+' сек. назад)'):'Ждём первый тик от экспортёра…';const ev=d.last_event_ts?new Date(d.last_event_ts):null;document.getElementById('fresh_detail').textContent='События в истории: всего '+d.total_rows.toLocaleString('ru-RU')+' · за час: '+d.events_last_hour+' · за сутки: '+d.events_last_24h+(ev?' · последнее: '+ev.toLocaleString('ru-RU'):'');}catch(e){}">
<span id="fresh_dot" style="width:10px;height:10px;border-radius:50%;background:#9ca3af;display:inline-block"></span><span id="fresh_text">Проверяю актуальность данных…</span><span class="muted" id="fresh_detail" style="margin-left:auto"></span></div>
<form id="filters" class="nfilters" hx-get="/admin/npr/rows" hx-trigger="input changed delay:300ms, change" hx-target="#rows" hx-swap="innerHTML" onsubmit="return false">
<label>Поиск по коду клиента<input type="search" name="q" placeholder="например, BR22…" value="{{.Q}}"></label>
<label>Тип клиента<select name="mt" multiple size="3">{{range $v := list "МД" "МД+" "С"}}<option value="{{$v}}" {{if has $.MT $v}}selected{{end}}>{{$v}}</option>{{end}}</select></label>
<label>УДС не менее<input type="number" step="0.01" name="umn" value="{{.UMN}}"></label>
<label>УДС не более<input type="number" step="0.01" name="umx" value="{{.UMX}}"></label>
<label style="align-self:center;white-space:nowrap;display:flex;gap:6px;align-items:center"><input type="checkbox" name="only_calls" value="true" {{if .OnlyCalls}}checked{{end}} style="width:auto"> только маржин-колл</label>
<label>Сортировка<select name="sort">{{range $k, $t := dict "uds_asc" "УДС ↑ (опаснее сверху)" "uds_desc" "УДС ↓" "client_asc" "Код клиента ↑" "client_desc" "Код клиента ↓" "ts_desc" "Время обновления ↓"}}<option value="{{$k}}" {{if eq $.Sort $k}}selected{{end}}>{{$t}}</option>{{end}}</select></label>
<input type="hidden" name="page" value="{{.Page}}"><input type="hidden" name="tab" value="{{.Tab}}">
</form>
<div id="rows" hx-get="/admin/npr/rows" hx-trigger="load, every 60s" hx-include="#filters" hx-swap="innerHTML"><p class="muted"><em>Загружаю клиентов…</em></p></div>
{{end}}