[598-25][@ant] fix: авто-seed карты ПВЗ при reboot:extra и sentinel-username для анонимных мутаций — убрать ручной psql INSERT после ребута и устранить лог-шум NOT NULL в mutation_logs

- boot: seedMarketplaceKuDetails (idempotent ON CONFLICT) + CLI seed:marketplace-ku
  + шаг в extra_reboot.sh после coopback с ожиданием создания таблицы;
  на свежем стенде krg/odn/myt (ACTIVE) появляются автоматически.
- controller: MutationLoggingInterceptor пишет username='anonymous' вместо
  undefined для мутаций без JWT (login/register) — INSERT в mutation_logs
  больше не падает на NOT NULL, лог анонимной мутации сохраняется для аудита.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-25 05:58:36 +00:00
parent 9016a3b4ec
commit dda4b480e0
5 changed files with 133 additions and 13 deletions
+1
View File
@@ -24,6 +24,7 @@
"boot:clean": "esno src/index.ts boot:clean",
"boot:extra": "esno src/index.ts boot:extra",
"boot:remote": "esno src/index.ts boot:remote",
"seed:marketplace-ku": "esno src/scripts/seed-marketplace-ku-details.ts",
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
"create-coop": "esno src/index.ts create-coop",
+20
View File
@@ -57,4 +57,24 @@ docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d coopback
# Контроллер создаёт marketplace-таблицы через TypeORM synchronize при старте.
# Ждём появления marketplace_ku_details, затем засеваем 3 ПВЗ Подмосковья
# (krg/odn/myt, ACTIVE) — иначе на свежем стенде таблица пуста, select ПВЗ
# приходит пустым/disabled и harness-сценарии ломаются. Сидер идемпотентен.
echo "Ждём создания marketplace_ku_details контроллером..."
KU_READY=""
for _ in $(seq 1 60); do
if docker compose exec -T postgres psql -U "${POSTGRES_USERNAME:-postgres}" -d "${POSTGRES_DATABASE:-voskhod}" -tAc "SELECT to_regclass('public.marketplace_ku_details')" 2>/dev/null | grep -q marketplace_ku_details; then
KU_READY=1
break
fi
sleep 2
done
if [ -n "$KU_READY" ]; then
echo "Засеваем 3 ПВЗ Подмосковья (krg/odn/myt)..."
pnpm run seed:marketplace-ku || echo "⚠ seed ПВЗ не выполнен (не критично для остального стенда)"
else
echo "⚠ marketplace_ku_details не появилась за отведённое время — пропускаем seed ПВЗ"
fi
echo "Перезапуск завершен!"
+84 -9
View File
@@ -1,6 +1,5 @@
/* eslint-disable node/prefer-global/process */
import { Client } from 'pg'
import type { Cooperative } from 'cooptypes'
export async function initSystemStatus() {
console.log('Инициализация статуса системы для coopname: voskhod')
@@ -157,6 +156,82 @@ export async function initUsersInPostgres(
}
}
/**
* Засев dev-фикстуры карты ПВЗ: 3 кооперативных участка Подмосковья
* (krg/odn/myt, status=ACTIVE) в таблицу `marketplace_ku_details`.
*
* Зачем: на свежем стенде после `reboot:extra` таблица пуста — select ПВЗ
* приходит пустым/disabled, harness-сценарии и UI выбора участка ломаются.
* Раньше строки заводили ручным `psql INSERT` после каждого reboot.
*
* Ordering: DDL `marketplace_ku_details` создаёт контроллер (TypeORM
* `synchronize:true`) при старте coopback — ПОЗЖЕ, чем boot:extra. Поэтому
* сидер вызывается из `extra_reboot.sh` уже ПОСЛЕ `up -d coopback`, а не из
* installExtraData; здесь дополнительно проверяем существование таблицы и
* мягко пропускаем seed, если контроллер ещё не создал её. Идемпотентно
* (`ON CONFLICT (coopname, core_braname) DO NOTHING`). Данные совпадают с
* `seed-marketplace-branches.ts` (адреса/контакты тех же krg/odn/myt).
*
* Не для прода: прод не запускает `extra_reboot.sh`, ПВЗ там заводит
* председатель через UI — этот сидер живёт только в dev-reboot-цепочке.
*/
export async function seedMarketplaceKuDetails(coopname = 'voskhod') {
const client = new Client({
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT || '5432'),
user: process.env.POSTGRES_USERNAME,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE,
})
// Будни 09:0018:00, суббота 10:00–16:00, воскресенье выходной.
const weekday = { open: '09:00', close: '18:00', breaks: [] as Array<{ start: string, end: string }> }
const workingHours = {
mon: weekday,
tue: weekday,
wed: weekday,
thu: weekday,
fri: weekday,
sat: { open: '10:00', close: '16:00', breaks: [] as Array<{ start: string, end: string }> },
}
const KU = [
{ braname: 'krg', address: 'Московская область, г. Красногорск, ул. Заводская, д. 1', phone: '+79991230101', email: 'krg@voskhod.coop' },
{ braname: 'odn', address: 'Московская область, г. Одинцово, ул. Центральная, д. 12', phone: '+79991230202', email: 'odn@voskhod.coop' },
{ braname: 'myt', address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5', phone: '+79991230303', email: 'myt@voskhod.coop' },
]
try {
await client.connect()
console.log('Подключение к PostgreSQL установлено для сидинга ПВЗ marketplace')
const exists = await client.query(`SELECT to_regclass('public.marketplace_ku_details') AS t`)
if (!exists.rows[0]?.t) {
console.warn('⚠ marketplace_ku_details ещё не создана контроллером — пропускаем seed ПВЗ')
return
}
for (const ku of KU) {
await client.query(`
INSERT INTO marketplace_ku_details
(coopname, core_braname, address_full, contact_phone, contact_email,
working_hours_json, status, geocode_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, 'ACTIVE', 'PENDING', now(), now())
ON CONFLICT (coopname, core_braname) DO NOTHING
`, [coopname, ku.braname, ku.address, ku.phone, ku.email, JSON.stringify(workingHours)])
}
console.log(`Засеяно ${KU.length} ПВЗ (krg/odn/myt, ACTIVE) в marketplace_ku_details для ${coopname}`)
}
catch (error) {
console.error('Ошибка сидинга ПВЗ marketplace в PostgreSQL:', error)
throw error
}
finally {
await client.end()
}
}
export async function initVaultInPostgres() {
const client = new Client({
host: process.env.POSTGRES_HOST,
@@ -241,9 +316,9 @@ export async function initExtensionsInPostgres() {
// Вставляем запись для capital extension
const capitalConfig = {
level_depth_base: 100000000,
onboarding_init_at: "2026-02-09T07:16:18.380Z",
onboarding_init_at: '2026-02-09T07:16:18.380Z',
expense_pool_percent: 100,
onboarding_expire_at: "2026-03-11T07:16:18.380Z",
onboarding_expire_at: '2026-03-11T07:16:18.380Z',
voting_period_in_days: 1,
authors_voting_percent: 62.8,
creators_voting_percent: 62.8,
@@ -253,15 +328,15 @@ export async function initExtensionsInPostgres() {
energy_decay_rate_per_day: 0.02,
coordinator_invite_validity_days: 30,
onboarding_blagorost_provision_done: true,
onboarding_blagorost_provision_hash: "CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A",
onboarding_blagorost_provision_hash: 'CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A',
onboarding_blagorost_offer_template_done: true,
onboarding_blagorost_offer_template_hash: "5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2",
onboarding_blagorost_offer_template_hash: '5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2',
onboarding_generator_offer_template_done: true,
onboarding_generator_offer_template_hash: "8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44",
onboarding_generator_offer_template_hash: '8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44',
onboarding_generator_program_template_done: true,
onboarding_generator_program_template_hash: "E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896",
onboarding_generator_program_template_hash: 'E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896',
onboarding_generation_contract_template_done: true,
onboarding_generation_contract_template_hash: "A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2",
onboarding_generation_contract_template_hash: 'A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2',
github_sync_branch: 'dev',
github_sync_poll_interval_minutes: 5,
github_api_token_encrypted: '',
@@ -282,7 +357,7 @@ export async function initExtensionsInPostgres() {
JSON.stringify(capitalConfig),
1,
new Date('2026-02-09T02:13:06.620Z'),
new Date('2026-02-09T02:27:57.155Z')
new Date('2026-02-09T02:27:57.155Z'),
])
// reports — встроенный extension, конфиг пустой (BuiltinSchema).
@@ -0,0 +1,17 @@
/* eslint-disable node/prefer-global/process */
/**
* CLI-обёртка для сидинга карты ПВЗ (3 КУ Подмосковья krg/odn/myt, ACTIVE)
* в `marketplace_ku_details`. Запускается из `extra_reboot.sh` ПОСЛЕ подъёма
* контроллера (он создаёт таблицу через TypeORM synchronize), чтобы свежий
* стенд сразу имел непустой select ПВЗ. Идемпотентно.
*
* pnpm --filter @coopenomics/boot run seed:marketplace-ku
*/
import { seedMarketplaceKuDetails } from '../postgres-init'
seedMarketplaceKuDetails()
.then(() => process.exit(0))
.catch((e) => {
console.error('seed:marketplace-ku failed:', e)
process.exit(1)
})
@@ -38,6 +38,13 @@ export class MutationLoggingInterceptor implements NestInterceptor {
const mutationName = info.fieldName;
const args = gqlContext.getArgs();
const user = ctx.req?.user;
// Публичные мутации (логин, регистрация) выполняются без JWT — у них нет
// user.username, а колонка mutation_logs.username NOT NULL. Пишем sentinel
// вместо undefined, иначе INSERT падает «null value in column username» на
// каждой анонимной мутации (sentinel вместо nullable-колонки — чтобы фикс не
// зависел от synchronize и работал на проде, где миграция эту колонку не
// трогала).
const username = user?.username ?? 'anonymous';
const startTime = Date.now();
// Санитизация аргументов - удаляем wif ключи
@@ -49,14 +56,14 @@ export class MutationLoggingInterceptor implements NestInterceptor {
// Логируем успешное выполнение
const duration = Date.now() - startTime;
this.logger.info(`[INTERCEPTOR] Мутация ${mutationName} выполнена успешно за ${duration}мс`, {
username: user?.username,
username,
coopname,
arguments: sanitizedArgs,
});
this.saveMutationLog({
coopname,
mutation_name: mutationName,
username: user?.username,
username,
arguments: sanitizedArgs,
duration_ms: duration,
status: 'success',
@@ -68,7 +75,7 @@ export class MutationLoggingInterceptor implements NestInterceptor {
// Логируем ошибку
const duration = Date.now() - startTime;
this.logger.info(`[INTERCEPTOR] Мутация ${mutationName} завершилась ошибкой за ${duration}мс: ${error.message}`, {
username: user?.username,
username,
coopname,
arguments: sanitizedArgs,
error: error.message,
@@ -76,7 +83,7 @@ export class MutationLoggingInterceptor implements NestInterceptor {
this.saveMutationLog({
coopname,
mutation_name: mutationName,
username: user?.username,
username,
arguments: sanitizedArgs,
duration_ms: duration,
status: 'error',