@mikitasazan/notify 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/events.d.ts CHANGED
@@ -12,13 +12,15 @@
12
12
  export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools';
13
13
  /**
14
14
  * Стабильный машинный ключ задачи — последняя строка каждой карточки, вида
15
- * `#проект/ключ`. По нему дневной разборщик сверяет «это 🔴 уже закрыто более
16
- * поздней карточкой того же ключа?» без сравнения человеческих формулировок,
17
- * которые меняются. Необязателен: без него ключ выводится из типа и заголовка
18
- * (см. `render.ts`), но выведенный наследует хрупкость формулировки — наши
19
- * регулярные отправители передают его явно. В вывод CLI (stderr, и его же
20
- * сторож на VPS читает объединённым потоком по словам `sent|failed|skipped`)
21
- * ключ не попадает никогда новое слово там ослепило бы сторожа.
15
+ * `#ключ` (без названия проекта: карточка и так лежит в форуме своего
16
+ * проекта `targets()` не шлёт её в чужой). По нему дневной разборщик
17
+ * сверяет «это 🔴 уже закрыто более поздней карточкой того же ключа?» без
18
+ * сравнения человеческих формулировок, которые меняются. Необязателен: без
19
+ * него ключ выводится из типа и заголовка (см. `render.ts`), но выведенный
20
+ * наследует хрупкость формулировки наши регулярные отправители передают
21
+ * его явно. В вывод CLI (stderr, и его же сторож на VPS читает объединённым
22
+ * потоком по словам `sent|failed|skipped`) ключ не попадает никогда — новое
23
+ * слово там ослепило бы сторожа.
22
24
  */
23
25
  type Keyed = {
24
26
  key?: string;
package/dist/render.js CHANGED
@@ -36,7 +36,10 @@ export const clampMessage = (text, limit = 4000) => {
36
36
  }
37
37
  const body = cut.slice(0, end);
38
38
  // Кламп мог отрезать закрывающие теги — добираем их, чтобы разметка сошлась.
39
- const tail = ['b', 'a', 'i', 'code']
39
+ // blockquote с приходом цитаты для примечаний/деталей длинный detail режется
40
+ // прямо посередине неё, и без этого тега Telegram отвечал бы 400 на незакрытую
41
+ // цитату (regex `<blockquote[ >]` ловит и вариант с атрибутом `expandable`).
42
+ const tail = ['b', 'a', 'i', 'code', 'blockquote']
40
43
  .filter((t) => {
41
44
  const opened = (body.match(new RegExp(`<${t}[ >]`, 'g')) ?? []).length;
42
45
  const closed = (body.match(new RegExp(`</${t}>`, 'g')) ?? []).length;
@@ -47,10 +50,23 @@ export const clampMessage = (text, limit = 4000) => {
47
50
  return `${body}${tail}\n…`;
48
51
  };
49
52
  const header = (icon, title, project) => `${icon} <b>${esc(title)}</b> · ${esc(project)}`;
50
- // Значение жирным: канон формата (bot-message-formatting-canon) иконка-лид,
51
- // факты построчно, значения выделены; метка остаётся обычной, чтобы глаз
52
- // цеплялся за содержимое, а не за служебное слово.
53
- const kv = (label, value) => value === undefined || value === '' ? null : `${esc(label)}: <b>${esc(value)}</b>`;
53
+ // Тире, не жирное значение: сплошной жирный текст в первой версии карточки
54
+ // читался как крик (жалоба владельца 18.08). Иконка-лид уже держит внимание
55
+ // на заголовке, факты идут построчно и без выделения — глаз сам находит
56
+ // цифру рядом с меткой.
57
+ const kv = (label, value) => value === undefined || value === '' ? null : `${esc(label)} — ${esc(value)}`;
58
+ // Длинное пояснение (примечание, детали инцидента) — цитатой: у Telegram это
59
+ // полоска слева и лёгкий отступ, читается как «подробности», а не как часть
60
+ // заголовка. Длиннее ~400 знаков — цитата сворачивается сама (`expandable`,
61
+ // Bot API), иначе стектрейс или дамп лога растягивает карточку на весь экран.
62
+ const EXPAND_AT = 400;
63
+ const note = (text) => {
64
+ if (!text) {
65
+ return null;
66
+ }
67
+ const body = esc(text);
68
+ return body.length > EXPAND_AT ? `<blockquote expandable>${body}</blockquote>` : `<blockquote>${body}</blockquote>`;
69
+ };
54
70
  const link = (url, label) => url ? `<a href="${esc(url)}">${esc(label)}</a>` : null;
55
71
  const join = (parts) => parts.filter((p) => p !== null).join('\n');
56
72
  /** Список позиций — общий для `job` и `report`, чтобы они не разъехались. */
@@ -70,7 +86,7 @@ const renderDeploy = (e) => {
70
86
  commitLine,
71
87
  kv('откуда', e.via),
72
88
  kv('куда', e.target),
73
- kv('примечание', e.note),
89
+ note(e.note),
74
90
  link(e.url, 'Открыть логи')
75
91
  ]);
76
92
  };
@@ -82,7 +98,7 @@ const renderJob = (e) => {
82
98
  ...(e.stats ?? []).map(([label, value]) => kv(label, value)),
83
99
  items.length > 0 ? '' : null,
84
100
  ...items,
85
- kv('примечание', e.note),
101
+ note(e.note),
86
102
  link(e.url, 'Подробнее')
87
103
  ]);
88
104
  };
@@ -146,14 +162,14 @@ const renderIssue = (e) => {
146
162
  link(e.url, 'Открыть задачу')
147
163
  ]);
148
164
  };
149
- const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title), e.detail ? esc(e.detail) : null, link(e.url, 'Подробнее')]);
165
+ const renderIncident = (e) => join([header('🚨', 'Инцидент', e.project), esc(e.title), note(e.detail), link(e.url, 'Подробнее')]);
150
166
  const renderHeartbeatMiss = (e) => join([
151
167
  header('🔴', `Не отметилась: ${e.job}`, e.project),
152
168
  kv('последний раз', e.lastSeen),
153
169
  kv('ожидалось', e.expected)
154
170
  ]);
155
171
  // Подпись файла — та же карточка, но лимит Telegram у caption свой: 1024.
156
- const renderFile = (e) => join([header('📄', e.title, e.project), kv('примечание', e.note)]);
172
+ const renderFile = (e) => join([header('📄', e.title, e.project), note(e.note)]);
157
173
  const RENDERERS = {
158
174
  deploy: renderDeploy,
159
175
  job: renderJob,
@@ -166,11 +182,15 @@ const RENDERERS = {
166
182
  file: renderFile
167
183
  };
168
184
  /**
169
- * Ключ задачи — последняя строка карточки: `#проект/ключ` в <code>. Явный
170
- * `key` побеждает; выведенный строится из заголовка и наследует хрупкость
171
- * формулировки регулярные отправители передают явный. Ключ переживает
172
- * MTProto-чтение (это простой текст, не разметка), по нему разборщик сверяет
173
- * 🔴 с более поздней успешной карточкой той же задачи.
185
+ * Ключ задачи — последняя строка карточки: `#ключ` курсивом в <code>. Без
186
+ * названия проекта: `targets()` никогда не шлёт карточку в чужой форум,
187
+ * проект и так на виду в заголовке («· mac-config»), а дублирующий префикс
188
+ * только растягивал тег на лишнюю строку в узком экране телефона (жалоба
189
+ * владельца 18.08). Явный `key` побеждает; выведенный строится из заголовка
190
+ * и наследует хрупкость формулировки — регулярные отправители передают явный.
191
+ * Ключ переживает MTProto-чтение (простой текст, не разметка), по нему
192
+ * разборщик сверяет 🔴 с более поздней успешной карточкой той же задачи —
193
+ * внутри чата одного проекта, где ключ и так уникален.
174
194
  */
175
195
  const slug = (raw) => raw
176
196
  .toLowerCase()
@@ -198,9 +218,9 @@ export const eventKey = (e) => {
198
218
  return e.type;
199
219
  }
200
220
  };
201
- return `#${slug(e.project)}/${e.key ? slug(e.key) : fallback()}`;
221
+ return e.key ? slug(e.key) : fallback();
202
222
  };
203
- const keyLine = (e) => `<code>${esc(eventKey(e))}</code>`;
223
+ const keyLine = (e) => `<i><code>#${esc(eventKey(e))}</code></i>`;
204
224
  /**
205
225
  * Рендерит событие в готовый HTML-текст, обрезанный под лимит Telegram.
206
226
  * Ключ добавляется ПОСЛЕ обрезки, с зарезервированным местом: обрезанная
package/dist/send.js CHANGED
@@ -309,6 +309,8 @@ export const sendReport = async (project, html, key) => {
309
309
  return 'skipped';
310
310
  }
311
311
  const forum = ROUTES[project];
312
- const tag = key ? `\n<code>#${project}/${key}</code>` : '';
312
+ // Без проекта как и render.ts: карточка уже лежит в форуме своего
313
+ // проекта, дублировать его в теге незачем.
314
+ const tag = key ? `\n<i><code>#${key}</code></i>` : '';
313
315
  return deliver([{ chat: forum.chat, thread: forum.ops, silent: true }], clampMessage(html) + tag);
314
316
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",