landing: - site.ts: убраны неиспользуемые roast, brew, priceDisplay, hero.eyebrow, footer.phone (Footer берёт CONTACT_PHONE) - form.ts: убраны FORM_TEXT.title/lead, CTAS, CtaPreset - StickyCta получает тексты пропсами от layout: клиентский чанк больше не тащит все тексты страницы - LeadForm/useLeadForm: без неиспользуемой подмены sendLead и className; leadPrefill без счётчика seq - Stamp без мёртвого tone; убраны data-interest, data-hot, класс lead-form (ничем не читались) - одно LEAD_FIELDS на sendLead и leadFormModel; регэксп typo вынесен из вызова; дубль фона body api: - changedFields и Version общие для leads и outbox; countLeads и leadStamps без копий SQL - errorMessage в system.ts вместо четырёх копий; take() для ожидающих подтверждений - backup: один Intl.DateTimeFormat на модуль, без невызываемых параметров timeZone/hour - без мёртвых LOOPBACK '::ffff:127.0.0.1', client.call, Pick<BackupCommands> со всеми ключами - testing.ts: validLead, addLead, count, leadIds вместо копий в тестах deploy.sh: deployed_field и цикл по REPOS вместо повторов имён
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
// Тема «Kvarta Cafe: Система»: сбои и служебные события без персональных данных.
|
||
// Всё дублируется в stdout — это единственный канал, если Telegram недоступен или тема не задана.
|
||
import type { TelegramClient } from './client.js';
|
||
|
||
export const SYSTEM_THROTTLE_MS = 10 * 60 * 1000;
|
||
|
||
export type SystemReporter = {
|
||
/** Никогда не бросает: отчёт о сбое не должен порождать новый сбой. */
|
||
report(kind: string, text: string): Promise<void>;
|
||
};
|
||
|
||
export function createSystemReporter({
|
||
client,
|
||
chatId,
|
||
threadId,
|
||
now,
|
||
log = (line: string) => console.log(line),
|
||
}: {
|
||
client?: Pick<TelegramClient, 'sendMessage'>;
|
||
chatId?: number;
|
||
threadId?: number;
|
||
now: () => Date;
|
||
log?: (line: string) => void;
|
||
}): SystemReporter {
|
||
// Одинаковые события не чаще раза в 10 минут; пропущенные копим и показываем с ближайшим отправленным.
|
||
const throttle = new Map<string, { sentAt: number; skipped: number }>();
|
||
|
||
return {
|
||
async report(kind, text) {
|
||
log(`[system] ${kind}: ${text}`);
|
||
if (!client || chatId === undefined || threadId === undefined) return;
|
||
|
||
const t = now().getTime();
|
||
const state = throttle.get(kind);
|
||
if (state && t - state.sentAt < SYSTEM_THROTTLE_MS) {
|
||
state.skipped++;
|
||
return;
|
||
}
|
||
const count = (state?.skipped ?? 0) + 1;
|
||
throttle.set(kind, { sentAt: t, skipped: 0 });
|
||
try {
|
||
await client.sendMessage({
|
||
chat_id: chatId,
|
||
message_thread_id: threadId,
|
||
text: count > 1 ? `×${count} за период · ${text}` : text,
|
||
});
|
||
} catch (err) {
|
||
log(`[system] не отправлено в Telegram: ${errorMessage(err)}`);
|
||
}
|
||
},
|
||
};
|
||
}
|
||
|
||
export function errorMessage(err: unknown): string {
|
||
return err instanceof Error ? err.message : String(err);
|
||
}
|
||
|
||
/** Короткий stack без путей до node_modules: в «Систему» и stdout. */
|
||
export function shortError(err: unknown): string {
|
||
if (!(err instanceof Error)) return String(err);
|
||
const frames = (err.stack ?? '')
|
||
.split('\n')
|
||
.slice(1)
|
||
.filter((l) => !l.includes('node_modules') && !l.includes('node:'))
|
||
.slice(0, 3)
|
||
.map((l) => l.trim());
|
||
return [`${err.name}: ${err.message}`, ...frames].join('\n');
|
||
}
|