Экспорт QUIK — отдельная служба bj-quikexport; модули, выделенные из ядра, ставятся с обновлением
bj-quikexport (modules/quikexport + cmd/bj-quikexport, 127.0.0.1:8084): управление ARQA quikexport перенесено из bj-server. Настройки у службы в /var/lib/bj/.bj/quik-export/settings.json (при первом запуске переносятся из setup.json), раздел отдаётся фрагментом с индикатором меню, действия отвечают X-BJ-Flash. bj-server встраивает раздел в «Настройку», проксирует формы /admin/setup/quikexport/… и передаёт лицензию (X-BJ-Licensed). Общая проверка служебного ключа — modules/modkit (им пользуется и bj-npr), общее для прокси — lkgateway/modules.go. Выделенные из ядра модули (bj-npr, bj-quikexport: Component.FromCore) при обновлении ставятся и там, где их ещё нет, если входят в лицензию: release.MarkSplitModules, request.json "new", root-установщик ставит и включает unit-файл из комплекта. Если в обновлении меняется сам установщик, модули ставятся после перезапуска bj-server (ensureSplitModules). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
[connect]
|
||||
trade-server-ip-address=127.0.0.1
|
||||
auth-name=xxx
|
||||
check-tmo=150
|
||||
port=15100
|
||||
user_name=xxx
|
||||
user_password=xxx
|
||||
AutoHeartBeat=1
|
||||
|
||||
[proxy]
|
||||
use-proxy=0
|
||||
proxy-server-ip-address=
|
||||
proxy-port=
|
||||
@@ -1,250 +0,0 @@
|
||||
-- Миграция: схема npr с историей НПР1/НПР2/УДС через триггер на "Portfolio".
|
||||
--
|
||||
-- Применять ПОСЛЕ того, как таблицы QUIK-экспортёра развёрнуты штатным скриптом
|
||||
-- quikexport_11/scripts/postgre/exporter_pg.sql
|
||||
-- в ту же БД. В эталонной "Portfolio" уже есть все нужные колонки
|
||||
-- (PortfolioValue, InitMargin, MinMargin, CorrectedMargin, FundsLevel, Status, RiskLevel),
|
||||
-- поэтому отдельного ALTER TABLE не требуется.
|
||||
--
|
||||
-- Политика записи: триггер пишет в npr.history только когда
|
||||
-- (а) клиент появился впервые, или
|
||||
-- (б) сменилась «зона» УДС (CALL → WARN → OK → INF), или
|
||||
-- (в) прошло более 30 мин с последней записи по этому клиенту (heartbeat).
|
||||
-- Это резко сокращает объём истории — фиксируются моменты смены состояния,
|
||||
-- а не каждый тик экспортёра. Дополняется политикой хранения (retention).
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- (б) Схема для истории НПР/УДС.
|
||||
CREATE SCHEMA IF NOT EXISTS npr;
|
||||
|
||||
-- (в) Таблица истории. Append-only: свой BIGSERIAL PK, без UNIQUE по клиенту.
|
||||
CREATE TABLE IF NOT EXISTS npr.history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
firm_id varchar(12) NOT NULL,
|
||||
client_code varchar(12) NOT NULL,
|
||||
limit_kind varchar(5) NOT NULL,
|
||||
curr_code varchar(4) NOT NULL,
|
||||
margin_type varchar(4) NULL,
|
||||
portfolio_value decimal(28, 8) NULL,
|
||||
init_margin float NULL,
|
||||
min_margin float NULL,
|
||||
corrected_margin float NULL,
|
||||
rcv1 float NULL,
|
||||
rcv2 float NULL,
|
||||
funds_level decimal(19, 2) NULL,
|
||||
status int NULL,
|
||||
risk_level int NULL
|
||||
);
|
||||
|
||||
-- (г) Индекс для выборок «история по клиенту за период».
|
||||
CREATE INDEX IF NOT EXISTS ix_npr_hist_client_ts
|
||||
ON npr.history (client_code, ts DESC);
|
||||
|
||||
-- «Пульс» от экспортёра: триггер обновляет last_tick на каждом тике (не чаще раза в 10 сек).
|
||||
-- Нужен отдельно от истории, чтобы UI видел «данные от QUIK приходят» даже когда зоны не меняются.
|
||||
CREATE TABLE IF NOT EXISTS npr.heartbeat (
|
||||
id int PRIMARY KEY DEFAULT 1,
|
||||
last_tick timestamptz NOT NULL DEFAULT NOW(),
|
||||
tick_count bigint NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT INTO npr.heartbeat (id) VALUES (1) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Кэш «последнее записанное состояние» — нужен триггеру, чтобы понять,
|
||||
-- изменилась ли зона относительно ранее сохранённой строки. Одна запись на клиентский PK.
|
||||
CREATE TABLE IF NOT EXISTS npr.last_seen (
|
||||
firm_id varchar(12) NOT NULL,
|
||||
client_code varchar(12) NOT NULL,
|
||||
limit_kind varchar(5) NOT NULL,
|
||||
curr_code varchar(12) NOT NULL,
|
||||
tag varchar(4) NOT NULL,
|
||||
last_ts timestamptz NOT NULL,
|
||||
last_zone text NOT NULL,
|
||||
last_uds numeric(19,2),
|
||||
PRIMARY KEY (firm_id, client_code, limit_kind, curr_code, tag)
|
||||
);
|
||||
|
||||
-- Зона УДС: CALL / WARN / OK / INF / UNKNOWN. Используется и триггером, и отчётами.
|
||||
CREATE OR REPLACE FUNCTION npr._zone(fl numeric) RETURNS text AS $$
|
||||
SELECT CASE
|
||||
WHEN fl IS NULL THEN 'UNKNOWN'
|
||||
WHEN fl >= 9.99 THEN 'INF'
|
||||
WHEN fl <= -9.99 THEN 'CALL'
|
||||
WHEN fl < 0 THEN 'CALL'
|
||||
WHEN fl < 1 THEN 'CALL'
|
||||
WHEN fl < 1.5 THEN 'WARN'
|
||||
ELSE 'OK'
|
||||
END;
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
-- (д) Триггер-функция: новая запись в history только при смене зоны или раз в 30 мин.
|
||||
-- Фильтр МД / МД+ / С применяется сразу, чтобы не тратить ресурсы на остальных.
|
||||
CREATE OR REPLACE FUNCTION npr.trg_portfolio_to_history()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $BODY$
|
||||
DECLARE
|
||||
new_zone text;
|
||||
last_ts timestamptz;
|
||||
last_zone text;
|
||||
should_write boolean := false;
|
||||
BEGIN
|
||||
-- Пульс: обновляем не чаще раза в 10 сек, чтобы не делать тысяч UPDATE на один тик экспортёра.
|
||||
UPDATE npr.heartbeat
|
||||
SET last_tick = NOW(), tick_count = tick_count + 1
|
||||
WHERE id = 1 AND last_tick < NOW() - INTERVAL '10 seconds';
|
||||
|
||||
IF NEW."MarginType" NOT IN ('МД','МД+','С') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
new_zone := npr._zone(NEW."FundsLevel");
|
||||
|
||||
SELECT ls.last_ts, ls.last_zone
|
||||
INTO last_ts, last_zone
|
||||
FROM npr.last_seen ls
|
||||
WHERE ls.firm_id = NEW."FirmId"
|
||||
AND ls.client_code = NEW."ClientCode"
|
||||
AND ls.limit_kind = NEW."LimitKind"
|
||||
AND ls.curr_code = NEW."CurrCode"
|
||||
AND ls.tag = NEW."Tag";
|
||||
|
||||
IF last_ts IS NULL THEN
|
||||
should_write := true; -- впервые видим клиента
|
||||
ELSIF last_zone IS DISTINCT FROM new_zone THEN
|
||||
should_write := true; -- переход зоны
|
||||
ELSIF NOW() - last_ts > INTERVAL '30 minutes' THEN
|
||||
should_write := true; -- heartbeat
|
||||
END IF;
|
||||
|
||||
IF should_write THEN
|
||||
INSERT INTO npr.history (
|
||||
firm_id, client_code, limit_kind, curr_code, margin_type,
|
||||
portfolio_value, init_margin, min_margin, corrected_margin,
|
||||
rcv1, rcv2, funds_level, status, risk_level
|
||||
) VALUES (
|
||||
NEW."FirmId", NEW."ClientCode", NEW."LimitKind", NEW."CurrCode", NEW."MarginType",
|
||||
NEW."PortfolioValue", NEW."InitMargin", NEW."MinMargin", NEW."CorrectedMargin",
|
||||
NEW."RCV1", NEW."RCV2", NEW."FundsLevel", NEW."Status", NEW."RiskLevel"
|
||||
);
|
||||
|
||||
INSERT INTO npr.last_seen (firm_id, client_code, limit_kind, curr_code, tag,
|
||||
last_ts, last_zone, last_uds)
|
||||
VALUES (NEW."FirmId", NEW."ClientCode", NEW."LimitKind", NEW."CurrCode", NEW."Tag",
|
||||
NOW(), new_zone, NEW."FundsLevel")
|
||||
ON CONFLICT (firm_id, client_code, limit_kind, curr_code, tag)
|
||||
DO UPDATE SET last_ts = EXCLUDED.last_ts,
|
||||
last_zone = EXCLUDED.last_zone,
|
||||
last_uds = EXCLUDED.last_uds;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
-- (е) Триггер на "Portfolio" — AFTER INSERT OR UPDATE.
|
||||
DROP TRIGGER IF EXISTS portfolio_to_npr_history ON "Portfolio";
|
||||
CREATE TRIGGER portfolio_to_npr_history
|
||||
AFTER INSERT OR UPDATE ON "Portfolio"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION npr.trg_portfolio_to_history();
|
||||
|
||||
-- (е2) Повторный INSERT в "Portfolio" → UPDATE без ошибки.
|
||||
-- Экспортёр каждый цикл сначала делает подготовленный INSERT по всем строкам
|
||||
-- и по дубликату ключа переходит к UPDATE. В PostgreSQL 11 каждая ошибка
|
||||
-- подготовленного INSERT оставляет ~20 КБ в памяти сессии: ~10 тыс. строк/мин
|
||||
-- дают ≈200 МБ/мин, backend дорастает до OOM и роняет весь кластер (14.09.2026).
|
||||
-- AFTER UPDATE триггер истории выше срабатывает как обычно.
|
||||
CREATE OR REPLACE FUNCTION npr.trg_portfolio_insert_as_update()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
UPDATE "Portfolio"
|
||||
SET "MarginType" = NEW."MarginType", "Status" = NEW."Status",
|
||||
"PortfolioValue" = NEW."PortfolioValue", "InitMargin" = NEW."InitMargin",
|
||||
"MinMargin" = NEW."MinMargin", "CorrectedMargin" = NEW."CorrectedMargin",
|
||||
"RCV1" = NEW."RCV1", "RCV2" = NEW."RCV2",
|
||||
"RiskLevel" = NEW."RiskLevel", "FundsLevel" = NEW."FundsLevel"
|
||||
WHERE "FirmId" = NEW."FirmId" AND "ClientCode" = NEW."ClientCode"
|
||||
AND "LimitKind" = NEW."LimitKind" AND "CurrCode" = NEW."CurrCode" AND "Tag" = NEW."Tag";
|
||||
IF FOUND THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
DROP TRIGGER IF EXISTS portfolio_insert_as_update ON "Portfolio";
|
||||
CREATE TRIGGER portfolio_insert_as_update
|
||||
BEFORE INSERT ON "Portfolio"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION npr.trg_portfolio_insert_as_update();
|
||||
|
||||
-- (ж) Прикладная вьюха: УДС с обработкой QUIK-сентинелов и маржинальное требование.
|
||||
-- QUIK кладёт УДС в диапазон [-9.99, 9.99]: 9.99 = «бесконечность» (капнут сверху),
|
||||
-- -9.99 = маржин-колл / принудзакрытие (капнут снизу).
|
||||
CREATE OR REPLACE VIEW npr.v_npr AS
|
||||
SELECT
|
||||
ts,
|
||||
firm_id,
|
||||
client_code,
|
||||
margin_type,
|
||||
portfolio_value,
|
||||
init_margin,
|
||||
min_margin,
|
||||
rcv1,
|
||||
rcv2,
|
||||
CASE WHEN funds_level >= 9.99 THEN NULL ELSE funds_level END AS uds,
|
||||
CASE WHEN funds_level >= 9.99 THEN 'INF'
|
||||
WHEN funds_level <= -9.99 THEN 'CALL'
|
||||
ELSE NULL END AS uds_flag,
|
||||
GREATEST(-rcv1, 0) AS margin_requirement
|
||||
FROM npr.history;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ===========================================================================
|
||||
-- Retention-политика: cron-файл /etc/cron.d/npr-retention чистит записи > 2 лет
|
||||
-- раз в месяц:
|
||||
-- 0 3 1 * * postgres psql -d quik_export -c \
|
||||
-- "DELETE FROM npr.history WHERE ts < NOW() - INTERVAL '2 years';"
|
||||
-- Если нужно партиционирование по месяцам — отдельная миграция.
|
||||
-- ===========================================================================
|
||||
|
||||
-- npr.v_current (текущее состояние по клиентам; снято с рабочей базы 14.09.2026)
|
||||
CREATE OR REPLACE VIEW npr.v_current AS
|
||||
SELECT p."FirmId" AS firm_id,
|
||||
p."ClientCode" AS client_code,
|
||||
p."LimitKind" AS limit_kind,
|
||||
p."CurrCode" AS curr_code,
|
||||
p."Tag" AS tag,
|
||||
p."MarginType" AS margin_type,
|
||||
p."RCV1" AS rcv1,
|
||||
p."RCV2" AS rcv2,
|
||||
p."FundsLevel" AS funds_level,
|
||||
p."PortfolioValue" AS portfolio_value,
|
||||
p."InitMargin" AS init_margin,
|
||||
p."MinMargin" AS min_margin,
|
||||
p."CorrectedMargin" AS corrected_margin,
|
||||
p."Status" AS status,
|
||||
p."RiskLevel" AS risk_level,
|
||||
CASE
|
||||
WHEN p."FundsLevel" >= 9.99 THEN NULL::numeric
|
||||
ELSE p."FundsLevel"
|
||||
END AS uds,
|
||||
CASE
|
||||
WHEN p."FundsLevel" >= 9.99 THEN 'INF'::text
|
||||
WHEN p."FundsLevel" <= '-9.99'::numeric THEN 'CALL'::text
|
||||
ELSE NULL::text
|
||||
END AS uds_flag,
|
||||
GREATEST(- p."RCV1", 0::double precision) AS margin_requirement,
|
||||
npr._zone(p."FundsLevel") AS zone,
|
||||
hb.last_tick AS ts
|
||||
FROM "Portfolio" p
|
||||
CROSS JOIN ( SELECT heartbeat.last_tick
|
||||
FROM npr.heartbeat
|
||||
WHERE heartbeat.id = 1) hb
|
||||
WHERE p."MarginType"::text = ANY (ARRAY['МД'::text, 'МД+'::text, 'С'::text, 'C'::text]);
|
||||
GRANT SELECT ON npr.v_current TO quik_ro;
|
||||
@@ -8,7 +8,7 @@
|
||||
<button data-sec="db"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.6"/><path d="M5 5.5v13c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6v-13"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6"/></svg></span>База данных<span class="ind {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span></button>
|
||||
{{if or .AdvancedUnlocked (index .Modules "quik-mnp")}}<button data-sec="fancy"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h12M12 4l4 4-4 4"/><path d="M20 16H8M12 20l-4-4 4-4"/></svg></span>Обмен Fancy<span class="ind {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{end}}"></span></button>
|
||||
<button data-sec="quik"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16"/><rect x="7" y="12" width="2.6" height="5"/><rect x="12" y="8" width="2.6" height="9"/><rect x="17" y="14" width="2.6" height="3"/></svg></span>QUIK МНП<span class="ind {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{end}}"></span></button>
|
||||
<button data-sec="quikexport"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l5-5 4 4 7-8"/><path d="M14 8h6v6"/></svg></span>Экспорт QUIK<span class="ind {{if not .QuikExport.Licensed}}err{{else if and .QuikExport.S.Enabled (eq .QuikExport.Service.Active "active")}}ok{{else if .QuikExport.S.Enabled}}warn{{end}}"></span></button>{{end}}
|
||||
<button data-sec="quikexport"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l5-5 4 4 7-8"/><path d="M14 8h6v6"/></svg></span>Экспорт QUIK<span class="ind {{.QuikExport.Ind}}"></span></button>{{end}}
|
||||
<button data-sec="users"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17" cy="9" r="2.5"/><path d="M15.5 14.5c2.8.3 5.5 2.5 5.5 5.5"/></svg></span>Пользователи<span class="ind {{if .Users.Open}}warn{{else}}ok{{end}}"></span></button>
|
||||
<button data-sec="crypto"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Криптография<span class="ind {{if .Settings.Crypto.Profile}}ok{{else}}warn{{end}}"></span></button>
|
||||
<button data-sec="nsd"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M4 21V10M20 21V10M3 10l9-6 9 6M8 21v-7M12 21v-7M16 21v-7"/></svg></span>НРД<span class="ind {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span></button>
|
||||
@@ -362,135 +362,7 @@
|
||||
|
||||
{{/* ============ ЭКСПОРТ БИРЖЕВОЙ ИНФОРМАЦИИ (QUIK) ============ */}}
|
||||
<section class="settings-section" id="sec-quikexport">
|
||||
{{$X := .QuikExport}}{{$S := $X.S}}
|
||||
<h1>QUIK — Экспорт биржевой информации</h1>
|
||||
<p class="muted">Модуль ARQA «Экспорт биржевой информации» выгружает данные сервера QUIK (портфели клиентов, позиции, сделки, справочники) в базу PostgreSQL. bj управляет им отсюда: ключи и логин QUIK, где держим базу, какой объём данных выгружаем, состояние службы и место на диске. На этих данных работают модуль «НПР / УДС» и дополнительные проверки M2M. Лицензионный признак <code>quik-export</code>.</p>
|
||||
{{range $X.Warnings}}<div class="card" style="padding:8px 14px;margin-bottom:8px;background:color-mix(in srgb,var(--warn) 10%,var(--card));display:flex;gap:8px;align-items:center"><span class="dot warn"></span>{{.}}</div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if not $X.Licensed}}err{{else if and $S.Enabled (eq $X.Service.Active "active")}}ok{{else if $S.Enabled}}warn{{else}}err{{end}}"></span>Состояние модуля</h2>
|
||||
<form method="post" action="/admin/setup/quikexport" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:12px">
|
||||
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="module">
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="enabled" value="1" {{if $S.Enabled}}checked{{end}} {{if not $X.Licensed}}disabled{{end}}> Модуль подключён</label>
|
||||
<button type="submit" class="btn" {{if not $X.Licensed}}disabled{{end}}>Сохранить</button>
|
||||
{{if not $X.Licensed}}<span class="muted">не входит в лицензию — см. раздел «Лицензия»</span>{{end}}
|
||||
</form>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
|
||||
<div class="nmini" style="padding:10px;border:1px solid var(--border);border-radius:8px"><div class="muted" style="font-size:11px;text-transform:uppercase">Служба quikexport</div>
|
||||
<strong>{{if not $X.Service.Installed}}не установлена{{else if eq $X.Service.Active "active"}}работает{{else}}{{$X.Service.Active}}{{end}}</strong>
|
||||
<div class="muted" style="font-size:11.5px">{{if $X.Service.PID}}PID {{$X.Service.PID}} · {{end}}{{if $X.Service.MemoryMB}}память {{$X.Service.MemoryMB}} МБ · {{end}}автозапуск {{if $X.Service.Enabled}}да{{else}}нет{{end}}{{if $X.Service.Restarts}} · перезапусков {{$X.Service.Restarts}}{{end}}</div></div>
|
||||
<div style="padding:10px;border:1px solid var(--border);border-radius:8px"><div class="muted" style="font-size:11px;text-transform:uppercase">Данные из QUIK</div>
|
||||
{{if $X.DB.OK}}<strong>{{if $X.DB.LastTick.IsZero}}нет пульса{{else if lt $X.DB.TickAgeSec 180}}свежие{{else}}устарели{{end}}</strong>
|
||||
<div class="muted" style="font-size:11.5px">{{if not $X.DB.LastTick.IsZero}}цикл {{msk $X.DB.LastTick}} ({{$X.DB.TickAgeSec}} с назад) · {{end}}клиентов {{$X.DB.Clients}}</div>
|
||||
{{else}}<strong style="color:var(--err)">база недоступна</strong><div class="muted" style="font-size:11.5px">{{$X.DB.Error}}</div>{{end}}</div>
|
||||
<div style="padding:10px;border:1px solid var(--border);border-radius:8px"><div class="muted" style="font-size:11px;text-transform:uppercase">Объём базы {{$S.DBName}}</div>
|
||||
<strong>{{printf "%.0f" $X.DB.DBSizeMB}} МБ</strong>
|
||||
<div class="muted" style="font-size:11.5px">история НПР {{$X.DB.HistoryRows}} строк{{if not $X.DB.HistoryFrom.IsZero}} с {{$X.DB.HistoryFrom.Format "02.01.2006"}}{{end}} · рост ≈ {{printf "%.1f" $X.DB.GrowthMBDay}} МБ/сутки ({{$X.DB.Rows24h}} событий за 24 ч)</div></div>
|
||||
<div style="padding:10px;border:1px solid var(--border);border-radius:8px"><div class="muted" style="font-size:11px;text-transform:uppercase">Диск {{if $X.DiskDB.OK}}базы данных{{else}}экспортёра{{end}}</div>
|
||||
{{with $X.DiskDB}}{{if .OK}}<strong style="{{if lt .FreeGB 10.0}}color:var(--err){{end}}">свободно {{printf "%.1f" .FreeGB}} ГБ из {{printf "%.0f" .TotalGB}}</strong><div class="muted" style="font-size:11.5px">занято {{.UsedPct}}% · {{.Path}}</div>{{end}}{{end}}
|
||||
{{if not $X.DiskDB.OK}}{{with $X.DiskExp}}<strong>свободно {{printf "%.1f" .FreeGB}} ГБ из {{printf "%.0f" .TotalGB}}</strong><div class="muted" style="font-size:11.5px">занято {{.UsedPct}}%</div>{{end}}{{end}}</div>
|
||||
<div style="padding:10px;border:1px solid var(--border);border-radius:8px"><div class="muted" style="font-size:11px;text-transform:uppercase">Логи и кэш экспортёра</div>
|
||||
<strong>{{printf "%.0f" $X.LogsMB}} МБ логов</strong>
|
||||
<div class="muted" style="font-size:11.5px">{{range $X.Logs}}{{if .Exists}}{{.Name}} {{printf "%.0f" .SizeMB}} МБ · {{end}}{{end}}кэш QAPI {{printf "%.0f" $X.QAPIMB}} МБ · ротация: ежедневно / 200 МБ, 5 копий</div></div>
|
||||
</div>
|
||||
{{if $X.DB.Tables}}<details style="margin-top:10px"><summary class="muted" style="cursor:pointer">Крупнейшие таблицы базы</summary>
|
||||
<table style="font-size:12px;margin-top:6px"><thead><tr><th>Таблица</th><th>Размер</th><th>Строк (оценка)</th></tr></thead><tbody>{{range $X.DB.Tables}}<tr><td><code>{{.Name}}</code></td><td>{{.Size}}</td><td>{{.Rows}}</td></tr>{{end}}</tbody></table></details>{{end}}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
|
||||
<form method="post" action="/admin/setup/quikexport/service"><input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="op" value="restart"><button class="btn" {{if not $X.Service.Installed}}disabled{{end}}>Перезапустить службу</button></form>
|
||||
<form method="post" action="/admin/setup/quikexport/service"><input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="op" value="{{if eq $X.Service.Active "active"}}stop{{else}}start{{end}}"><button class="btn" {{if not $X.Service.Installed}}disabled{{end}}>{{if eq $X.Service.Active "active"}}Остановить{{else}}Запустить{{end}}</button></form>
|
||||
{{if and $X.Service.Installed (not $X.Service.Enabled)}}<form method="post" action="/admin/setup/quikexport/service"><input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="op" value="enable"><button class="btn">Включить автозапуск</button></form>{{end}}
|
||||
</div>
|
||||
{{if $X.LogTail}}<details style="margin-top:10px"><summary class="muted" style="cursor:pointer">Последние строки export.log{{if $X.ErrTail}} и export.err{{end}}</summary>
|
||||
<pre style="font-size:11px;white-space:pre-wrap;max-height:260px;overflow:auto;background:var(--card-2);padding:8px;border-radius:6px">{{range $X.LogTail}}{{.}}
|
||||
{{end}}{{if $X.ErrTail}}--- export.err ---
|
||||
{{range $X.ErrTail}}{{.}}
|
||||
{{end}}{{end}}</pre></details>{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if and $S.QuikHost $S.QuikUser $S.KeysNames}}ok{{else}}warn{{end}}"></span>1. Подключение к серверу QUIK</h2>
|
||||
<p class="muted">Логин экспорта на сервере QUIK брокера (выдаёт администратор QUIK / ARQA) и пара ключей <code>pubring.txk</code> / <code>secring.txk</code> к нему. Одна учётная запись = одна сессия: второй экспортёр с тем же логином получит «Вы уже работаете в системе».</p>
|
||||
<form method="post" action="/admin/setup/quikexport" style="display:grid;grid-template-columns:2fr 1fr;gap:10px;max-width:720px;margin-top:10px">
|
||||
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="quik">
|
||||
<label>Адрес сервера QUIK <input type="text" name="quik_host" value="{{$S.QuikHost}}" placeholder="91.209.122.88" style="width:100%" required></label>
|
||||
<label>Порт <input type="number" name="quik_port" value="{{$S.QuikPort}}" style="width:100%"></label>
|
||||
<label>Пользователь QUIK <input type="text" name="quik_user" value="{{$S.QuikUser}}" style="width:100%" required></label>
|
||||
<label>Пароль <input type="password" name="quik_password" value="" placeholder="{{if $S.QuikPassword}}(сохранён, оставьте пустым){{else}}пароль{{end}}" style="width:100%"></label>
|
||||
<label>Имя авторизации (auth-name) <input type="text" name="auth_name" value="{{$S.AuthName}}" placeholder="FontV" style="width:100%"></label>
|
||||
<div style="align-self:end"><button type="submit" class="btn btn-ok">Сохранить</button></div>
|
||||
</form>
|
||||
<form method="post" action="/admin/setup/quikexport/keys" enctype="multipart/form-data" style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;max-width:720px;margin-top:14px;align-items:end">
|
||||
<input type="hidden" name="section" value="sec-quikexport">
|
||||
<label>Открытый ключ pubring.txk <input type="file" name="pubring" accept=".txk" style="width:100%"></label>
|
||||
<label>Закрытый ключ secring.txk <input type="file" name="secring" accept=".txk" style="width:100%"></label>
|
||||
<button type="submit" class="btn">Загрузить ключи</button>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-top:6px">Ключи: {{if $S.KeysNames}}{{range $S.KeysNames}}<code>{{.}}</code> {{end}}{{if not $S.KeysAt.IsZero}}(загружены {{msk $S.KeysAt}}){{end}}{{else}}не загружены{{end}}.
|
||||
В каталоге экспортёра: {{range $X.Files}}{{.Name}} {{if .Exists}}✓ {{.ModTime.Format "02.01 15:04"}}{{else}}✗{{end}} · {{end}}</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if and $S.DBPassword $S.ROPassword}}ok{{else}}warn{{end}}"></span>2. База данных экспорта</h2>
|
||||
<p class="muted">Куда экспортёр пишет данные. Обычно — наш PostgreSQL на этом же сервере (bj создаст базу, роли и схему одной кнопкой). Внешний сервер — если базу держит заказчик. Логин только для чтения используют модуль «НПР / УДС», проверки M2M и внешние системы (Grafana, отчёты).</p>
|
||||
<form method="post" action="/admin/setup/quikexport" style="display:grid;gap:10px;max-width:720px;margin-top:10px">
|
||||
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="db">
|
||||
<div style="display:flex;gap:18px;flex-wrap:wrap">
|
||||
<label style="display:flex;gap:6px;align-items:center;font-weight:600;margin:0"><input type="radio" name="db_mode" value="local" {{if ne $S.DBMode "external"}}checked{{end}} onchange="document.getElementById('qx-ext').style.display='none'"> Наш PostgreSQL (этот сервер)</label>
|
||||
<label style="display:flex;gap:6px;align-items:center;font-weight:600;margin:0"><input type="radio" name="db_mode" value="external" {{if eq $S.DBMode "external"}}checked{{end}} onchange="document.getElementById('qx-ext').style.display='grid'"> Внешний сервер PostgreSQL</label>
|
||||
</div>
|
||||
<div id="qx-ext" style="display:{{if eq $S.DBMode "external"}}grid{{else}}none{{end}};grid-template-columns:2fr 1fr;gap:10px">
|
||||
<label>Хост <input type="text" name="db_host" value="{{$S.DBHost}}" style="width:100%"></label>
|
||||
<label>Порт <input type="number" name="db_port" value="{{$S.DBPort}}" style="width:100%"></label>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
|
||||
<label>База <input type="text" name="db_name" value="{{$S.DBName}}" style="width:100%"></label>
|
||||
<label>Пользователь экспорта (запись) <input type="text" name="db_user" value="{{$S.DBUser}}" style="width:100%"></label>
|
||||
<label>Пароль <input type="password" name="db_password" value="" placeholder="{{if $S.DBPassword}}(сохранён){{else}}задать{{end}}" style="width:100%"></label>
|
||||
<label>Логин только для чтения <input type="text" name="ro_user" value="{{$S.ROUser}}" style="width:100%"></label>
|
||||
<label>Пароль <input type="password" name="ro_password" value="" placeholder="{{if $S.ROPassword}}(сохранён){{else}}задать{{end}}" style="width:100%"></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button>
|
||||
<button type="submit" formaction="/admin/setup/quikexport/dbinit" class="btn" {{if eq $S.DBMode "external"}}disabled{{end}}>Создать базу и роли на этом сервере</button>
|
||||
<a class="btn" href="/admin/setup/quikexport/sql">SQL для внешнего сервера</a></div>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-top:8px">Доступ для внешних систем и других модулей: <code>{{if $X.ShowRO}}{{$X.RODSNFull}}{{else}}{{$X.RODSN}}{{end}}</code>
|
||||
{{if $X.ShowRO}}<a href="/admin/setup#quikexport">скрыть пароль</a>{{else}}<a href="/admin/setup?reveal_ro=1#quikexport">показать пароль</a>{{end}}{{if ne $S.DBMode "external"}} · снаружи: хост <code>{{$X.ServerIP}}</code>, доступ по IP открывает администратор (pg_hba){{end}}.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot ok"></span>3. Объём данных</h2>
|
||||
<p class="muted">Какие таблицы QUIK выгружаем. Чем больше набор, тем больше нагрузка на сервер QUIK, база и диск. Портфели пересчитываются каждые «период портфелей» секунд (ARQA рекомендует не менее 60); история изменений НПР/УДС хранится в схеме <code>npr</code>.</p>
|
||||
<form method="post" action="/admin/setup/quikexport" style="display:grid;gap:10px;margin-top:10px">
|
||||
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="volume">
|
||||
<div style="display:grid;gap:6px">{{range $X.Presets}}
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0"><input type="radio" name="preset" value="{{.Key}}" {{if eq $S.Preset .Key}}checked{{end}} onchange="document.getElementById('qx-tables').style.display=this.value==='custom'?'block':'none'"><span><b>{{.Title}}</b><br><span class="muted" style="font-size:12px">{{.Desc}}</span></span></label>{{end}}
|
||||
</div>
|
||||
<div id="qx-tables" style="display:{{if eq $S.Preset "custom"}}block{{else}}none{{end}};border:1px solid var(--border);border-radius:8px;padding:10px">
|
||||
{{range $g := $X.Groups}}<div style="font-weight:600;margin:6px 0 4px">{{$g}}</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:4px 12px">{{range $X.Tables}}{{if eq .Group $g}}
|
||||
<label style="display:flex;gap:6px;align-items:flex-start;font-weight:400;margin:0;font-size:12.5px"><input type="checkbox" name="tables" value="{{.Name}}" {{if index $X.Enabled .Name}}checked{{end}}><span><code>{{.Name}}</code> — {{.Title}} <span class="muted">· объём: {{.Load}}{{if .Note}} · {{.Note}}{{end}}</span></span></label>{{end}}{{end}}</div>{{end}}
|
||||
</div>
|
||||
<details><summary class="muted" style="cursor:pointer">Сейчас включено: {{len $S.Tables}} таблиц — {{range $S.Tables}}<code>{{.}}</code> {{end}}</summary></details>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:820px">
|
||||
<label>Период портфелей, с <input type="number" name="portfolio_period" min="10" value="{{$S.PortfolioPeriod}}" style="width:100%"></label>
|
||||
<label>Период «Текущих торгов», с <input type="number" name="param_period" min="0" value="{{$S.ParamPeriod}}" style="width:100%"></label>
|
||||
<label>Виды лимитов (LimitKinds) <input type="text" name="limit_kinds" value="{{$S.LimitKinds}}" style="width:100%"></label>
|
||||
<label>Уровень лога (DebugLevel) <input type="number" name="debug_level" min="0" max="255" value="{{$S.DebugLevel}}" style="width:100%"></label>
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-ok">Сохранить</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if not $S.AppliedAt.IsZero}}ok{{else}}warn{{end}}"></span>4. Применить к экспортёру</h2>
|
||||
<p class="muted">bj собирает <code>export.ini</code>, <code>info.ini</code>, секцию ODBC и ключи из настроек выше и устанавливает их в каталог службы (<code>{{$S.Dir}}</code>) с резервной копией прежних файлов, настраивает ротацию логов и перезапускает экспортёр. {{if not $S.AppliedAt.IsZero}}Последнее применение: {{msk $S.AppliedAt}}.{{end}}{{if not $S.Imported}} Если экспортёр уже настроен вручную — сначала «Прочитать настройки из экспортёра».{{end}}</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
||||
<a class="btn" href="/admin/setup/quikexport/preview" target="_blank">Предпросмотр файлов</a>
|
||||
<form method="post" action="/admin/setup/quikexport/apply" onsubmit="return confirm('Установить настройки в экспортёр и перезапустить службу? Выгрузка прервётся на несколько секунд.')"><input type="hidden" name="section" value="sec-quikexport"><button class="btn btn-ok" {{if not $X.Licensed}}disabled{{end}}>Применить настройки и перезапустить экспортёр</button></form>
|
||||
<form method="post" action="/admin/setup/quikexport/import"><input type="hidden" name="section" value="sec-quikexport"><button class="btn" {{if not $X.Service.Installed}}disabled{{end}}>Прочитать настройки из экспортёра</button></form>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px;margin-top:10px">Установка на новом сервере: дистрибутив ARQA (install_quikexport-12.x.run, Linux) ставится установщиком bj в <code>{{$S.Dir}}</code>; нужны пакеты unixodbc, odbc-postgresql, libjemalloc2. Обновления экспортёра приходят через канал обновлений bj после проверки у нас.</p>
|
||||
</div>
|
||||
{{.QuikExport.HTML}}
|
||||
</section>
|
||||
|
||||
{{/* ============ КРИПТОГРАФИЯ ============ */}}
|
||||
@@ -923,16 +795,16 @@
|
||||
|
||||
<div class="card">
|
||||
<h2>Компоненты</h2>
|
||||
<p class="muted">Каждый компонент обновляется отдельно: своя версия в релизе, перезапускается только его служба. Файлы ставит root-установщик после повторной проверки подписи и контрольных сумм. Модули, которых нет на этом сервере, обновлением не ставятся.</p>
|
||||
<p class="muted">Каждый компонент обновляется отдельно: своя версия в релизе, перезапускается только его служба. Файлы ставит root-установщик после повторной проверки подписи и контрольных сумм. Модули, которых нет на этом сервере, обновлением не ставятся — кроме выделенных из ядра (НПР / УДС, экспорт QUIK): если модуль входит в лицензию, он ставится вместе с обновлением, иначе его раздел перестал бы работать.</p>
|
||||
<div style="overflow-x:auto"><table style="margin-top:8px">
|
||||
<thead><tr><th>Компонент</th><th>Установлена</th><th>Доступна</th><th>Служба</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Components}}<tr>
|
||||
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code></div></td>
|
||||
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
|
||||
<td>{{if .Available}}{{.Available}}{{if .Update}} <span class="hero-status warn">новее</span>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
|
||||
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
|
||||
<td><code style="font-size:11px">{{.Unit}}</code></td>
|
||||
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Обновить компонент до {{.Available}}? Его служба перезапустится.');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">Обновить</button></form>{{end}}</td>
|
||||
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
|
||||
</tr>{{end}}
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
Reference in New Issue
Block a user