@sayue_ltr/fleq 1.51.0 → 2.0.1

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/dist/config.js +2 -2
  4. package/dist/dmdata/telegram-parser.js +78 -5
  5. package/dist/engine/cli/cli-run.js +19 -4
  6. package/dist/engine/eew/eew-tracker.js +41 -15
  7. package/dist/engine/monitor/monitor.js +1 -1
  8. package/dist/engine/notification/notifier.js +5 -3
  9. package/dist/engine/notification/sound-player.js +232 -35
  10. package/dist/engine/presentation/processors/process-eew.js +15 -0
  11. package/dist/engine/presentation/processors/process-message.js +2 -2
  12. package/dist/engine/startup/update-checker.js +23 -2
  13. package/dist/engine/template/compiler.js +4 -1
  14. package/dist/engine/template/field-accessor.js +10 -4
  15. package/dist/engine/template/filters.js +14 -8
  16. package/dist/engine/template/parser.js +10 -15
  17. package/dist/types.js +1 -1
  18. package/dist/ui/earthquake-formatter.js +5 -0
  19. package/dist/ui/formatter.js +49 -0
  20. package/dist/ui/repl-handlers/command-definitions.js +11 -4
  21. package/dist/ui/repl-handlers/info-handlers.js +97 -41
  22. package/dist/ui/repl-handlers/settings-handlers.js +19 -15
  23. package/dist/ui/statistics-formatter.js +65 -15
  24. package/dist/ui/status-line.js +11 -0
  25. package/dist/ui/test-samples.js +6 -0
  26. package/dist/ui/theme.js +9 -0
  27. package/dist/ui/waiting-tips-eew.js +63 -0
  28. package/dist/ui/waiting-tips-info-systems.js +81 -0
  29. package/dist/ui/waiting-tips-seismology.js +97 -0
  30. package/dist/ui/waiting-tips-tsunami.js +72 -0
  31. package/dist/ui/waiting-tips-weather.js +189 -0
  32. package/dist/ui/waiting-tips.js +137 -6
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,92 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [2.0.1](https://github.com/Lateo2580/FlEq/compare/v2.0.0...v2.0.1) (2026-05-03)
6
+
7
+
8
+ ### バグ修正
9
+
10
+ * **update-checker:** scoped package 名の `/` を %2F に encode する ([6f02a0e](https://github.com/Lateo2580/FlEq/commit/6f02a0e5e9536272d1c4760575f10c7c93cc10d3))
11
+ * **update-checker:** scoped パッケージ名対応と personal build 検出 ([b4c75d8](https://github.com/Lateo2580/FlEq/commit/b4c75d8cbec707255e6e8d50a688dee415217f15))
12
+
13
+ ## [2.0.0](https://github.com/Lateo2580/FlEq/compare/v1.51.0...v2.0.0) (2026-04-25)
14
+
15
+
16
+ ### ⚠ BREAKING CHANGES
17
+
18
+ * **policy:** --event-log / --event-log-raw / --no-event-log の
19
+ CLI オプションが廃止された。eventLog / eventLogRaw 設定キーも廃止。
20
+ これらの機能に依存している外部利用者は、削除前バージョン (v1.52.x)
21
+ を pin するか、自身で private fork を保持して継続利用すること。
22
+
23
+ 次回リリースは major bump (v2.0.0) として npm run release:major で
24
+ 発行する想定。
25
+
26
+ 関連: 段階1 (baf0b41)
27
+
28
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
29
+
30
+ ### 機能追加
31
+
32
+ * **cli:** add --event-log and --event-log-raw flags ([f046653](https://github.com/Lateo2580/FlEq/commit/f046653df9c42d88f0362bdeb34dc87bcdf999f3))
33
+ * **cli:** show eventLog status in startup banner ([07d509f](https://github.com/Lateo2580/FlEq/commit/07d509fda93cfc0e4f1d83b9208e5dc0e86e9270))
34
+ * **cli:** 起動バナーに音声バックエンドの健康状態を表示 ([30a070e](https://github.com/Lateo2580/FlEq/commit/30a070e0fa97c8c034269c608a050500b058ce6d))
35
+ * clock コマンド・ヘルプ・status 表示を uptime 対応に ([f362927](https://github.com/Lateo2580/FlEq/commit/f362927c84515770b609628c2ced7ba0cf19004f))
36
+ * commands コマンド新設 — help から一覧表示を分離 ([0eb81c1](https://github.com/Lateo2580/FlEq/commit/0eb81c1fe335617e6346657a0d9bf1cacca0b387))
37
+ * **config:** add eventLog and eventLogRaw configuration ([db1f376](https://github.com/Lateo2580/FlEq/commit/db1f376648e705c61d1439f85e75fdde17a74810))
38
+ * EEW isWarning 判定を XML ベースに移行 (classification はフォールバック) ([c2087cd](https://github.com/Lateo2580/FlEq/commit/c2087cd931a31af603cadd4eff90a0a33bac833f))
39
+ * Event File Writer — write telegrams as individual JSON files ([0ebbd0c](https://github.com/Lateo2580/FlEq/commit/0ebbd0cb21e33d03cb97f7c6a78f996941dbfcad))
40
+ * formatUptime 関数を追加 (DDD:HH:MM:SS, dim ゼロ桁) ([a694522](https://github.com/Lateo2580/FlEq/commit/a6945227cc1c3c1d16728356075aae4e9765301b))
41
+ * implement EventFileWriter with atomic write ([e05e9cb](https://github.com/Lateo2580/FlEq/commit/e05e9cb66cd61e9af049b2c6e8d2beed7c1f50e8))
42
+ * **monitor:** apply eventLog config to EventFileWriter on startup ([12c251b](https://github.com/Lateo2580/FlEq/commit/12c251b1c298d4f8f0446ec93576d8904bed091b))
43
+ * ParsedEarthquakeInfo に eventId を追加し、地震情報表示に EventID 行を表示 ([7562377](https://github.com/Lateo2580/FlEq/commit/756237726821e2c48ceed4a58ba2ce62ce3270b8))
44
+ * **policy:** dmdata.jp 再配信ポリシー対応の段階1実装 ([baf0b41](https://github.com/Lateo2580/FlEq/commit/baf0b4135878ab6400554bc7c790ddd4c8be6cdc))
45
+ * processEew に suppressed kind を追加 (VXSE45 優先時) ([e65b33e](https://github.com/Lateo2580/FlEq/commit/e65b33e6d7ab7480bd13359a7da21661b78a40a7))
46
+ * PromptClock 型に uptime を追加 ([d7c2829](https://github.com/Lateo2580/FlEq/commit/d7c2829487b78ebf993c4188ec1054d74b8ad071))
47
+ * **repl:** add eventlog command ([874a679](https://github.com/Lateo2580/FlEq/commit/874a679eb4b12d8bba330a205d11a117ab8de2e9))
48
+ * **sound:** checkSoundBackend で実再生プローブによる健康チェックを追加 ([46c391c](https://github.com/Lateo2580/FlEq/commit/46c391cb3222e5200594e27040f2f9214aace123))
49
+ * **sound:** process.uptime ベースの単調時計ヘルパーを追加 ([6cfc195](https://github.com/Lateo2580/FlEq/commit/6cfc195bf2540f0c6586ad22be8159824fdde49f))
50
+ * **sound:** 起動直後 60 秒以内の再生失敗を 20 秒後に自動リトライ ([9b86ff9](https://github.com/Lateo2580/FlEq/commit/9b86ff96111bc18066e0f4aaba7f0f513fd07b1e))
51
+ * **sound:** 再生失敗系ログを debug から warn に格上げ ([fd9f1d5](https://github.com/Lateo2580/FlEq/commit/fd9f1d5dc07ba234bbdb9cb0cb76bd9015662673))
52
+ * StatusLine で uptime モード表示に対応 (未接続時も表示) ([55c93c7](https://github.com/Lateo2580/FlEq/commit/55c93c7733d5e31fccd5f47944e37736aaa5ec41))
53
+ * wire EventFileWriter into message-router and VolcanoRouteHandler ([a1107e0](https://github.com/Lateo2580/FlEq/commit/a1107e00db7adbfdd81c3e377143001c7542e8ce))
54
+ * wrap-up コマンド新設 + Codex CLI フラグ更新 + commit リマインド hook 追加 ([68dac86](https://github.com/Lateo2580/FlEq/commit/68dac861fc17e95ce1fb6ac9cb0c43bc5bc62592))
55
+ * 警報昇格通知をイベント単位の isUpgradeToWarning に変更 ([2e338dd](https://github.com/Lateo2580/FlEq/commit/2e338dd797f4c814da8209505f2d1e2a822e951b))
56
+ * 待機中Tipsに火山カテゴリを追加 (+78項目) ([0f619bb](https://github.com/Lateo2580/FlEq/commit/0f619bb87eb663f611ea461d2002040a0fedafe6))
57
+ * 待機中Tipsに気象カテゴリを追加 (警報・予報・定時報) ([deda85e](https://github.com/Lateo2580/FlEq/commit/deda85e46f1b154248a919ac623c3e81ea5d1048))
58
+ * 待機中Tipsを大幅拡充 — 新8カテゴリ+229件追加 (444→673件) ([bac458a](https://github.com/Lateo2580/FlEq/commit/bac458a556ddaa807f558fcf57cc0199b10c2a40))
59
+ * 統計表示にテーマロールベースのカラーリングを追加 ([d816b17](https://github.com/Lateo2580/FlEq/commit/d816b1766f3b25c2cbd199ee54248df995ef3121))
60
+
61
+
62
+ ### バグ修正
63
+
64
+ * **cli:** let --no-event-log override --event-log-raw ([fe5226e](https://github.com/Lateo2580/FlEq/commit/fe5226e13ccf7d3c8357c925cdff5466755a340f))
65
+ * **eew:** isWarning 観測ログを真の仕様不整合のみに絞る ([be1757c](https://github.com/Lateo2580/FlEq/commit/be1757cabf7ae22b7c8045e316477ad338fac049))
66
+ * **event-file-writer:** prevent collision and enforce maxFiles strictly ([a7227d6](https://github.com/Lateo2580/FlEq/commit/a7227d6f6b619fcd5a5ddfbf972aa143a5c5ff63))
67
+ * formatUptime の日部分を文字レベルで dim 表示する ([67d5c70](https://github.com/Lateo2580/FlEq/commit/67d5c700a35d48b97de2c7534392ef33109af59c))
68
+ * **monitor:** drain EventFileWriter queue on shutdown ([6cd0fc2](https://github.com/Lateo2580/FlEq/commit/6cd0fc2836e51c43218202ea51257a18cdb8056b))
69
+ * processMessage の EEW suppressed 結果ハンドリングを追加 ([3b5f48f](https://github.com/Lateo2580/FlEq/commit/3b5f48f72e627ee0e68372e892959c623bab174b))
70
+ * **sound:** Codex レビュー 2 の指摘を反映 ([3d3fdf1](https://github.com/Lateo2580/FlEq/commit/3d3fdf185a51c43d6519c7fb083a415ffa184249))
71
+ * **sound:** DoneHandle 化で launch 経路の log/bell も二重完了ガードで保護 ([cc5fd1c](https://github.com/Lateo2580/FlEq/commit/cc5fd1c126a38c39585250dd15831f436b2a02b7))
72
+ * **sound:** runPlay の timeout と execFile コールバックによる二重完了を防止 ([b0456dc](https://github.com/Lateo2580/FlEq/commit/b0456dc8f78ea1b7e6d6f62bcc8e4a007fb90aae))
73
+ * VXSE61 の複数 Coordinate ノードから十進度を正しく抽出 ([bdcfb38](https://github.com/Lateo2580/FlEq/commit/bdcfb383f0d34694714fa81da38cee875529e424))
74
+ * 起動メッセージをcommands案内に更新、+マーカーの位置と凡例を改善 ([f13fabd](https://github.com/Lateo2580/FlEq/commit/f13fabd7c688ef9da5b171787de51e5aa80d2c00))
75
+ * 抑制報の hasWarningIssued 更新と終端処理を修正 (Codex レビュー指摘) ([34676ba](https://github.com/Lateo2580/FlEq/commit/34676ba8a5c0d434de387a13b7349740abaaeafb))
76
+
77
+
78
+ ### リファクタリング
79
+
80
+ * codex-design スキルを対立的レビュー方式に刷新 ([6db5a6d](https://github.com/Lateo2580/FlEq/commit/6db5a6d6dd17ca8bcdf0192064458dc866f92752))
81
+ * EewTracker を byType Map ベースに再設計 ([79a606f](https://github.com/Lateo2580/FlEq/commit/79a606fbcb80fe2b15ee5887529140ec9c311f56))
82
+ * **policy:** dmdata.jp 再配信ポリシー対応の段階2 ([cf5c6fb](https://github.com/Lateo2580/FlEq/commit/cf5c6fbe047953ce256827e18ce5a62d74186096))
83
+
84
+
85
+ ### ドキュメント
86
+
87
+ * EEW再設計に伴う仕様書同期 (5ファイル) ([a4f4b4a](https://github.com/Lateo2580/FlEq/commit/a4f4b4ad15f2c764599078884e50a427f678aa08))
88
+ * uptime モードの Tip・仕様書を同期 ([6b77ae5](https://github.com/Lateo2580/FlEq/commit/6b77ae57298584e7978bdae40cc3c2b86e4ae084))
89
+ * 待機中Tipsを現行機能に同期 (21件追加, 1件修正) ([078e070](https://github.com/Lateo2580/FlEq/commit/078e0708b76e2a89246dac67a2242cd3387558ea))
90
+
5
91
  ## [1.51.0](https://github.com/Lateo2580/FlEq/compare/v1.50.1...v1.51.0) (2026-03-29)
6
92
 
7
93
 
package/README.md CHANGED
@@ -202,7 +202,7 @@ fleq config keys # 設定可能キー一覧を表示
202
202
  | `promptClock` | プロンプト時計: `"elapsed"` (経過時間) / `"clock"` (現在時刻) |
203
203
  | `waitTipIntervalMin` | 待機中ヒント表示間隔(分、0 で無効、デフォルト: 30) |
204
204
  | `sound` | 通知音の有効/無効 (`true` / `false`) |
205
- | `eewLog` | EEW ログ記録の有効/無効 (`true` / `false`) |
205
+ | `eewLog` | EEW ログ記録の有効/無効 (`true` / `false`、デフォルト: `false`。明示 opt-in) |
206
206
  | `maxObservations` | 観測点の最大表示件数 (1〜999 / `"off"` で全件表示) |
207
207
  | `backup` | EEW 副回線の有効/無効 (`true` / `false`) |
208
208
  | `nightMode` | ナイトモードの有効/無効 (`true` / `false`) |
package/dist/config.js CHANGED
@@ -159,7 +159,7 @@ const VALID_TEST_MODES = ["no", "including", "only"];
159
159
  /** 有効な表示モード */
160
160
  const VALID_DISPLAY_MODES = ["normal", "compact"];
161
161
  /** 有効なプロンプト時計モード */
162
- const VALID_PROMPT_CLOCKS = ["elapsed", "clock"];
162
+ const VALID_PROMPT_CLOCKS = ["elapsed", "clock", "uptime"];
163
163
  /** 有効な EEW ログ記録項目 */
164
164
  exports.VALID_EEW_LOG_FIELDS = [
165
165
  "hypocenter",
@@ -214,7 +214,7 @@ const CONFIG_KEYS = {
214
214
  tableWidth: 'テーブル表示幅 (40〜200 / "auto" でターミナル幅に自動追従)',
215
215
  infoFullText: "お知らせ電文の全文表示 (true/false)",
216
216
  displayMode: '表示モード: "normal" | "compact"',
217
- promptClock: 'プロンプト時計: "elapsed" (経過時間) | "clock" (現在時刻)',
217
+ promptClock: 'プロンプト時計: "elapsed" (経過時間) | "clock" (現在時刻) | "uptime" (稼働時間)',
218
218
  waitTipIntervalMin: "待機中ヒント表示間隔 (分, 0で無効)",
219
219
  sound: "通知音の有効/無効 (true/false)",
220
220
  eewLog: "EEWログ記録の有効/無効 (true/false)",
@@ -152,10 +152,15 @@ function extractEarthquake(earthquake) {
152
152
  const area = first(dig(hypo, "Area"));
153
153
  const name = str(dig(area, "Name"));
154
154
  // 座標パース: "+35.7+139.8-10000/" 形式
155
- const coordStr = str(dig(area, "jmx_eb:Coordinate", "#text") ||
156
- dig(area, "Coordinate", "#text") ||
157
- dig(area, "jmx_eb:Coordinate") ||
158
- dig(area, "Coordinate"));
155
+ // VXSE61 等では jmx_eb:Coordinate が複数 (十進度 + 度分) 存在し配列になる。
156
+ // type="震源位置(度分)" を除外して十進度を優先選択する。
157
+ const rawCoord = dig(area, "jmx_eb:Coordinate") || dig(area, "Coordinate");
158
+ const coordNode = Array.isArray(rawCoord)
159
+ ? rawCoord.find((c) => str(dig(c, "@_type")) !== "震源位置(度分)") ?? rawCoord[0]
160
+ : rawCoord;
161
+ const coordStr = str(coordNode != null && typeof coordNode === "object"
162
+ ? dig(coordNode, "#text")
163
+ : coordNode);
159
164
  const { lat, lon, depth } = parseCoordinate(coordStr);
160
165
  const magRaw = str(dig(earthquake, "jmx_eb:Magnitude", "#text") ||
161
166
  dig(earthquake, "Magnitude", "#text") ||
@@ -238,6 +243,54 @@ function extractTsunami(body) {
238
243
  return { text };
239
244
  }
240
245
  // ── EEW ヘルパー ──
246
+ /** Headline Information の Kind Code に警報コード (31) が含まれるか */
247
+ function hasWarningHeadlineCode(head) {
248
+ const headline = dig(head, "Headline");
249
+ const informations = dig(headline, "Information");
250
+ const infoList = Array.isArray(informations) ? informations : informations ? [informations] : [];
251
+ for (const info of infoList) {
252
+ const items = dig(info, "Item");
253
+ const itemList = Array.isArray(items) ? items : items ? [items] : [];
254
+ for (const item of itemList) {
255
+ const kinds = dig(item, "Kind");
256
+ const kindList = Array.isArray(kinds) ? kinds : kinds ? [kinds] : [];
257
+ for (const kind of kindList) {
258
+ const code = parseInt(str(dig(kind, "Code")), 10);
259
+ if (code === 31)
260
+ return true;
261
+ }
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+ /** 予測地域の Category Kind Code に警報コード (10-19) が含まれるか */
267
+ function hasWarningAreaKind(body) {
268
+ const forecast = dig(body, "Intensity", "Forecast");
269
+ if (!forecast)
270
+ return false;
271
+ const prefs = dig(forecast, "Pref");
272
+ if (!Array.isArray(prefs))
273
+ return false;
274
+ for (const pref of prefs) {
275
+ const areas = dig(pref, "Area");
276
+ if (!Array.isArray(areas))
277
+ continue;
278
+ for (const area of areas) {
279
+ const categories = dig(area, "Category");
280
+ const catList = Array.isArray(categories) ? categories : categories ? [categories] : [];
281
+ for (const cat of catList) {
282
+ const kinds = dig(cat, "Kind");
283
+ const kindList = Array.isArray(kinds) ? kinds : kinds ? [kinds] : [];
284
+ for (const kind of kindList) {
285
+ const code = parseInt(str(dig(kind, "Code")), 10);
286
+ if (code >= 10 && code <= 19)
287
+ return true;
288
+ }
289
+ }
290
+ }
291
+ }
292
+ return false;
293
+ }
241
294
  function parseMaxIntChangeReason(body) {
242
295
  const raw = str(dig(body, "Intensity", "Forecast", "Appendix", "MaxIntChangeReason"));
243
296
  if (!raw)
@@ -418,6 +471,7 @@ function parseEarthquakeTelegram(msg) {
418
471
  reportDateTime: str(dig(head, "ReportDateTime")),
419
472
  headline: str(dig(head, "Headline", "Text")) || null,
420
473
  publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
474
+ eventId: str(dig(head, "EventID")) || null,
421
475
  isTest: msg.head.test,
422
476
  };
423
477
  // 震源
@@ -461,7 +515,7 @@ function parseEewTelegram(msg) {
461
515
  serial: str(dig(head, "Serial")) || null,
462
516
  eventId: str(dig(head, "EventID")) || null,
463
517
  isTest: msg.head.test,
464
- isWarning: msg.classification === "eew.warning",
518
+ isWarning: false, // 仮値 — 後で XML から判定
465
519
  isAssumedHypocenter: false,
466
520
  };
467
521
  info.maxIntChangeReason = parseMaxIntChangeReason(body);
@@ -480,6 +534,25 @@ function parseEewTelegram(msg) {
480
534
  (info.maxIntChangeReason === 9 || hasPlumArea);
481
535
  info.isAssumedHypocenter =
482
536
  assumedHypocenterByCondition || assumedHypocenterByFallback;
537
+ // isWarning: XML ベース主判定 + classification を安全側フォールバック
538
+ // xmlWarning / classWarning を先に変数化し、判定と観測ログで同じ値を共用する。
539
+ const xmlWarning = msg.head.type === "VXSE43" ||
540
+ hasWarningAreaKind(body) ||
541
+ hasWarningHeadlineCode(head);
542
+ const classWarning = msg.classification === "eew.warning";
543
+ info.isWarning = xmlWarning || classWarning;
544
+ // 観測ログ (仕様不整合の検知用):
545
+ // (1) classification=eew.warning だが XML ベース判定で警報条件を検出できない
546
+ // (2) VXSE43 電文なのに classification が eew.warning ではない(契約差分・仕様変更の兆候)
547
+ // 逆方向の一般形 (xmlWarning && !classWarning) は VXSE44/VXSE45 警報相当の正常パターンなのでログしない。
548
+ if (classWarning && !xmlWarning) {
549
+ log.warn(`EEW classification=eew.warning だが XML ベース判定で警報条件を検出できず: ` +
550
+ `type=${msg.head.type} EventID=${str(dig(head, "EventID"))}`);
551
+ }
552
+ else if (msg.head.type === "VXSE43" && !classWarning) {
553
+ log.warn(`EEW VXSE43 電文だが classification=${msg.classification} (eew.warning ではない): ` +
554
+ `EventID=${str(dig(head, "EventID"))}`);
555
+ }
483
556
  // NextAdvisory (最終報)
484
557
  const nextAdvisory = str(dig(body, "NextAdvisory"));
485
558
  if (nextAdvisory) {
@@ -50,7 +50,7 @@ const updateChecker = __importStar(require("../startup/update-checker"));
50
50
  const log = __importStar(require("../../logger"));
51
51
  const pipeline_controller_1 = require("../filter-template/pipeline-controller");
52
52
  // eslint-disable-next-line @typescript-eslint/no-var-requires
53
- const { version: VERSION } = require("../../../package.json");
53
+ const { version: VERSION, name: PACKAGE_NAME } = require("../../../package.json");
54
54
  async function runMonitor(opts) {
55
55
  // ログレベル設定
56
56
  if (opts.debug) {
@@ -162,8 +162,8 @@ async function runMonitor(opts) {
162
162
  log.info(`定期要約: ${opts.summaryInterval}分間隔`);
163
163
  }
164
164
  }
165
- printBanner(config);
166
- updateChecker.checkForUpdates("fleq", VERSION);
165
+ await printBanner(config);
166
+ updateChecker.checkForUpdates(PACKAGE_NAME, VERSION);
167
167
  await (0, monitor_1.startMonitor)(config, pipelineController);
168
168
  }
169
169
  /** ターミナルタイトルを設定する (ANSI OSC sequence) */
@@ -180,12 +180,27 @@ function resetTerminalTitle() {
180
180
  }
181
181
  }
182
182
  /** 起動バナー表示 */
183
- function printBanner(config) {
183
+ async function printBanner(config) {
184
184
  log.info(`受信区分: ${config.classifications.join(", ")}`);
185
185
  log.info(`テストモード: ${config.testMode}`);
186
186
  if (config.displayMode !== "normal") {
187
187
  log.info(`表示モード: ${config.displayMode}`);
188
188
  }
189
+ // 音声バックエンド状態 (起動バナー末尾、Linux は実再生プローブ)
190
+ try {
191
+ const { checkSoundBackend } = await Promise.resolve().then(() => __importStar(require("../notification/sound-player")));
192
+ const result = await checkSoundBackend();
193
+ // CUD パレット: blueGreen = RGB(0, 158, 115), vermillion = RGB(213, 94, 0)
194
+ const line = result.ok
195
+ ? chalk_1.default.rgb(0, 158, 115)(`音声: ${result.label} OK`)
196
+ : chalk_1.default.rgb(213, 94, 0)(`音声: ${result.label} NG${result.reason ? ` (${result.reason})` : ""}`);
197
+ console.log(line);
198
+ }
199
+ catch (err) {
200
+ if (err instanceof Error) {
201
+ log.warn(`音声バックエンド確認エラー: ${err.message}`);
202
+ }
203
+ }
189
204
  log.info("接続を開始します...");
190
205
  console.log();
191
206
  }
@@ -118,6 +118,8 @@ class EewTracker {
118
118
  isNew: true,
119
119
  isDuplicate: false,
120
120
  isCancelled: info.infoType === "取消",
121
+ isSuppressed: false,
122
+ isUpgradeToWarning: false,
121
123
  activeCount: this.getActiveCount(),
122
124
  colorIndex: 0,
123
125
  };
@@ -125,56 +127,80 @@ class EewTracker {
125
127
  const serialRaw = parseInt(info.serial || "", 10);
126
128
  const serial = Number.isFinite(serialRaw) ? serialRaw : null;
127
129
  const isCancelled = info.infoType === "取消";
130
+ const headType = info.type;
128
131
  const existing = this.events.get(eventId);
129
132
  if (existing) {
130
- // 既知のイベント 報数チェック
131
- if (!isCancelled && serial != null && serial > 0 && serial <= existing.lastSerial) {
132
- // 同じか古い報数 重複
133
+ const typeState = existing.byType.get(headType);
134
+ // 同一 type 内の重複判定
135
+ if (!isCancelled && serial != null && serial > 0 && typeState && serial <= typeState.lastSerial) {
133
136
  return {
134
137
  isNew: false,
135
138
  isDuplicate: true,
136
139
  isCancelled: false,
140
+ isSuppressed: false,
141
+ isUpgradeToWarning: false,
137
142
  activeCount: this.getActiveCount(),
138
143
  colorIndex: existing.colorIndex,
139
144
  };
140
145
  }
141
- // 差分計算
142
- const diff = existing.previousInfo ? computeDiff(existing.previousInfo, info) : undefined;
143
- const previousInfo = existing.previousInfo;
144
- // 状態更新
145
- if (serial != null) {
146
- existing.lastSerial = Math.max(existing.lastSerial, serial);
146
+ // 抑制判定: VXSE45 受信済みなら VXSE43/44 は抑制
147
+ const isSuppressed = existing.hasSeen45 && (headType === "VXSE43" || headType === "VXSE44");
148
+ // type 状態の更新 (抑制されても serial・lastUpdate は更新する)
149
+ const previousInfo = typeState?.previousInfo;
150
+ if (!typeState) {
151
+ existing.byType.set(headType, { lastSerial: serial ?? 0, previousInfo: info });
152
+ }
153
+ else {
154
+ if (serial != null) {
155
+ typeState.lastSerial = Math.max(typeState.lastSerial, serial);
156
+ }
157
+ typeState.previousInfo = info;
158
+ }
159
+ // hasSeen45 更新
160
+ if (headType === "VXSE45") {
161
+ existing.hasSeen45 = true;
162
+ }
163
+ // 差分計算: 同一 type 内の連続更新でのみ (初めての type では diff なし)
164
+ const diff = previousInfo ? computeDiff(previousInfo, info) : undefined;
165
+ // 警報昇格判定 (イベント単位)
166
+ const isUpgradeToWarning = !isSuppressed && !existing.hasWarningIssued && info.isWarning;
167
+ if (!isSuppressed) {
168
+ existing.hasWarningIssued = existing.hasWarningIssued || info.isWarning;
147
169
  }
148
- existing.isWarning = existing.isWarning || info.isWarning;
149
170
  existing.isCancelled = isCancelled;
150
171
  existing.lastUpdate = new Date();
151
- existing.previousInfo = info;
152
172
  return {
153
173
  isNew: false,
154
174
  isDuplicate: false,
155
175
  isCancelled,
176
+ isSuppressed,
177
+ isUpgradeToWarning,
156
178
  activeCount: this.getActiveCount(),
157
- diff,
179
+ diff: isSuppressed ? undefined : diff,
158
180
  previousInfo,
159
181
  colorIndex: existing.colorIndex,
160
182
  };
161
183
  }
162
184
  // 新規イベント
163
185
  const colorIndex = this.nextColorIndex();
186
+ const byType = new Map();
187
+ byType.set(headType, { lastSerial: serial ?? 0, previousInfo: info });
164
188
  this.events.set(eventId, {
165
189
  eventId,
166
- lastSerial: serial ?? 0,
167
- isWarning: info.isWarning,
190
+ byType,
191
+ hasSeen45: headType === "VXSE45",
192
+ hasWarningIssued: info.isWarning,
168
193
  isCancelled,
169
194
  isFinalized: false,
170
195
  lastUpdate: new Date(),
171
- previousInfo: info,
172
196
  colorIndex,
173
197
  });
174
198
  return {
175
199
  isNew: true,
176
200
  isDuplicate: false,
177
201
  isCancelled,
202
+ isSuppressed: false,
203
+ isUpgradeToWarning: false,
178
204
  activeCount: this.getActiveCount(),
179
205
  colorIndex,
180
206
  };
@@ -76,7 +76,7 @@ async function startMonitor(config, pipelineController) {
76
76
  }
77
77
  log.info(chalk_1.default.green("リアルタイム受信中..."));
78
78
  if (isFirstConnection) {
79
- log.info(chalk_1.default.gray("help でコマンド一覧を表示"));
79
+ log.info(chalk_1.default.gray("commands (短縮: cmds) でコマンド一覧を表示"));
80
80
  isFirstConnection = false;
81
81
  }
82
82
  (0, repl_coordinator_1.updateReplConnectionState)(replHandler, true);
@@ -173,10 +173,12 @@ class Notifier {
173
173
  notifyEew(info, result) {
174
174
  if (!this.settings.eew)
175
175
  return;
176
- // 通知条件: 第1報 / 予報→警報切替 / 取消報 / 最終報
177
- const isUpgradeToWarning = result.previousInfo?.isWarning === false && info.isWarning === true;
176
+ // 抑制された報は通知しない
177
+ if (result.isSuppressed)
178
+ return;
179
+ // 通知条件: 第1報 / 警報昇格 / 取消報 / 最終報
178
180
  const isFinal = info.nextAdvisory != null;
179
- if (!result.isNew && !isUpgradeToWarning && !result.isCancelled && !isFinal) {
181
+ if (!result.isNew && !result.isUpgradeToWarning && !result.isCancelled && !isFinal) {
180
182
  return;
181
183
  }
182
184
  if (result.isCancelled) {