@sayue_ltr/fleq 1.50.1 → 2.0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +42 -6
  3. package/dist/config.js +37 -4
  4. package/dist/dmdata/rest-client.js +58 -3
  5. package/dist/dmdata/telegram-parser.js +115 -64
  6. package/dist/dmdata/ws-client.js +49 -18
  7. package/dist/engine/cli/cli-run.js +88 -3
  8. package/dist/engine/cli/cli.js +12 -0
  9. package/dist/engine/eew/eew-tracker.js +41 -15
  10. package/dist/engine/filter/compile-filter.js +21 -0
  11. package/dist/engine/filter/compiler.js +188 -0
  12. package/dist/engine/filter/errors.js +41 -0
  13. package/dist/engine/filter/field-registry.js +78 -0
  14. package/dist/engine/filter/index.js +15 -0
  15. package/dist/engine/filter/parser.js +137 -0
  16. package/dist/engine/filter/rank-maps.js +34 -0
  17. package/dist/engine/filter/tokenizer.js +121 -0
  18. package/dist/engine/filter/type-checker.js +104 -0
  19. package/dist/engine/filter/types.js +2 -0
  20. package/dist/engine/filter-template/pipeline-controller.js +73 -0
  21. package/dist/engine/filter-template/pipeline.js +16 -0
  22. package/dist/engine/messages/display-callbacks.js +7 -0
  23. package/dist/engine/messages/message-router.js +114 -182
  24. package/dist/engine/messages/summary-tracker.js +106 -0
  25. package/dist/engine/messages/telegram-stats.js +103 -0
  26. package/dist/engine/messages/volcano-route-handler.js +122 -0
  27. package/dist/engine/monitor/monitor.js +52 -4
  28. package/dist/engine/monitor/shutdown.js +1 -0
  29. package/dist/engine/notification/notifier.js +21 -4
  30. package/dist/engine/notification/sound-player.js +398 -36
  31. package/dist/engine/presentation/diff-store.js +158 -0
  32. package/dist/engine/presentation/diff-types.js +2 -0
  33. package/dist/engine/presentation/events/from-earthquake.js +53 -0
  34. package/dist/engine/presentation/events/from-eew.js +72 -0
  35. package/dist/engine/presentation/events/from-lg-observation.js +58 -0
  36. package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
  37. package/dist/engine/presentation/events/from-raw.js +35 -0
  38. package/dist/engine/presentation/events/from-seismic-text.js +37 -0
  39. package/dist/engine/presentation/events/from-tsunami.js +51 -0
  40. package/dist/engine/presentation/events/from-volcano.js +88 -0
  41. package/dist/engine/presentation/events/to-presentation-event.js +32 -0
  42. package/dist/engine/presentation/level-helpers.js +118 -0
  43. package/dist/engine/presentation/processors/process-earthquake.js +36 -0
  44. package/dist/engine/presentation/processors/process-eew.js +105 -0
  45. package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
  46. package/dist/engine/presentation/processors/process-message.js +53 -0
  47. package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
  48. package/dist/engine/presentation/processors/process-raw.js +22 -0
  49. package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
  50. package/dist/engine/presentation/processors/process-tsunami.js +42 -0
  51. package/dist/engine/presentation/processors/process-volcano.js +41 -0
  52. package/dist/engine/presentation/types.js +2 -0
  53. package/dist/engine/startup/config-resolver.js +2 -0
  54. package/dist/engine/template/compile-template.js +18 -0
  55. package/dist/engine/template/compiler.js +105 -0
  56. package/dist/engine/template/field-accessor.js +31 -0
  57. package/dist/engine/template/filters.js +100 -0
  58. package/dist/engine/template/index.js +5 -0
  59. package/dist/engine/template/parser.js +185 -0
  60. package/dist/engine/template/tokenizer.js +96 -0
  61. package/dist/engine/template/types.js +2 -0
  62. package/dist/types.js +3 -2
  63. package/dist/ui/display-adapter.js +60 -0
  64. package/dist/ui/earthquake-formatter.js +22 -5
  65. package/dist/ui/eew-formatter.js +25 -10
  66. package/dist/ui/formatter.js +116 -32
  67. package/dist/ui/minimap/grid-layout.js +91 -0
  68. package/dist/ui/minimap/index.js +16 -0
  69. package/dist/ui/minimap/minimap-renderer.js +277 -0
  70. package/dist/ui/minimap/pref-mapping.js +82 -0
  71. package/dist/ui/minimap/types.js +2 -0
  72. package/dist/ui/night-overlay.js +56 -0
  73. package/dist/ui/repl-handlers/command-definitions.js +327 -0
  74. package/dist/ui/repl-handlers/index.js +11 -0
  75. package/dist/ui/repl-handlers/info-handlers.js +633 -0
  76. package/dist/ui/repl-handlers/operation-handlers.js +233 -0
  77. package/dist/ui/repl-handlers/settings-handlers.js +927 -0
  78. package/dist/ui/repl-handlers/types.js +10 -0
  79. package/dist/ui/repl.js +81 -1752
  80. package/dist/ui/statistics-formatter.js +258 -0
  81. package/dist/ui/status-line.js +80 -0
  82. package/dist/ui/summary/index.js +5 -0
  83. package/dist/ui/summary/summary-line.js +18 -0
  84. package/dist/ui/summary/summary-model.js +31 -0
  85. package/dist/ui/summary/token-builders.js +317 -0
  86. package/dist/ui/summary/types.js +2 -0
  87. package/dist/ui/summary/width-fit.js +41 -0
  88. package/dist/ui/summary-interval-formatter.js +72 -0
  89. package/dist/ui/test-samples.js +6 -0
  90. package/dist/ui/theme.js +43 -5
  91. package/dist/ui/tip-shuffler.js +81 -0
  92. package/dist/ui/volcano-formatter.js +15 -13
  93. package/dist/ui/waiting-tips-eew.js +63 -0
  94. package/dist/ui/waiting-tips-info-systems.js +81 -0
  95. package/dist/ui/waiting-tips-seismology.js +97 -0
  96. package/dist/ui/waiting-tips-tsunami.js +72 -0
  97. package/dist/ui/waiting-tips-weather.js +189 -0
  98. package/dist/ui/waiting-tips.js +420 -249
  99. package/package.json +1 -1
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.typeCheck = typeCheck;
4
+ const field_registry_1 = require("./field-registry");
5
+ const errors_1 = require("./errors");
6
+ /** AST を走査し、フィールド参照と演算子の型整合を検証する */
7
+ function typeCheck(ast, source) {
8
+ switch (ast.kind) {
9
+ case "or":
10
+ case "and":
11
+ for (const child of ast.children)
12
+ typeCheck(child, source);
13
+ break;
14
+ case "not":
15
+ typeCheck(ast.operand, source);
16
+ break;
17
+ case "truthy":
18
+ validateFieldExists(ast.value);
19
+ break;
20
+ case "comparison":
21
+ validateComparison(ast.left, ast.op, ast.right, source);
22
+ break;
23
+ }
24
+ }
25
+ function validateFieldExists(node) {
26
+ if (node.kind === "path") {
27
+ const name = node.segments.join(".");
28
+ const field = (0, field_registry_1.resolveField)(name);
29
+ if (field == null) {
30
+ throw new errors_1.FilterFieldError(name, (0, field_registry_1.fieldNames)());
31
+ }
32
+ }
33
+ }
34
+ function validateComparison(left, op, right, source) {
35
+ // パスが左辺にある場合のフィールド検証
36
+ if (left.kind === "path") {
37
+ const name = left.segments.join(".");
38
+ const field = (0, field_registry_1.resolveField)(name);
39
+ if (field == null) {
40
+ throw new errors_1.FilterFieldError(name, (0, field_registry_1.fieldNames)());
41
+ }
42
+ // enum 型に対する数値リテラルチェック
43
+ if (field.kind === "enum:intensity" && right.kind === "number") {
44
+ throw new errors_1.FilterTypeError(`型が不一致: ${name} ${op} ${right.value}\n` +
45
+ `\`${name}\` は震度文字列("1", "5-", "6+"等)で比較する。数値リテラルは使えない`);
46
+ }
47
+ if (field.kind === "enum:lgInt" && right.kind === "number") {
48
+ throw new errors_1.FilterTypeError(`型が不一致: ${name} ${op} ${right.value}\n` +
49
+ `\`${name}\` は長周期階級文字列("0"〜"4")で比較する。数値リテラルは使えない`);
50
+ }
51
+ // 順序比較の型検証
52
+ if (op === "<" || op === "<=" || op === ">" || op === ">=") {
53
+ if (!field.supportsOrder) {
54
+ throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は順序比較に対応していない`);
55
+ }
56
+ }
57
+ // regex 演算子の検証
58
+ if (op === "~" || op === "!~") {
59
+ if (field.kind !== "string" && field.kind !== "enum:frameLevel" && field.kind !== "enum:intensity" && field.kind !== "enum:lgInt") {
60
+ throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は正規表現マッチに対応していない`);
61
+ }
62
+ if (right.kind === "string") {
63
+ try {
64
+ new RegExp(right.value);
65
+ }
66
+ catch {
67
+ throw new errors_1.FilterTypeError(`正規表現が不正だ: "~" の右辺 "${right.value}" を解釈できない`);
68
+ }
69
+ if (isRedosRisk(right.value)) {
70
+ throw new errors_1.FilterTypeError(`正規表現が危険だ: "${right.value}" は入れ子の量指定子を含んでおり、ReDoS の恐れがある`);
71
+ }
72
+ }
73
+ }
74
+ // in の検証: 右辺はリストでなければならない
75
+ if (op === "in") {
76
+ if (right.kind !== "list") {
77
+ throw new errors_1.FilterTypeError(`\`in\` の右辺にはリスト [...] が必要`);
78
+ }
79
+ }
80
+ // contains の検証
81
+ if (op === "contains") {
82
+ if (field.kind !== "string[]" && field.kind !== "number[]" && field.kind !== "string") {
83
+ throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は contains に対応していない`);
84
+ }
85
+ // 右辺はリテラル (string/number) でなければならない
86
+ if (right.kind !== "string" && right.kind !== "number") {
87
+ throw new errors_1.FilterTypeError(`\`contains\` の右辺にはリテラル (文字列または数値) が必要`);
88
+ }
89
+ }
90
+ }
91
+ // 右辺のパスも検証
92
+ if (right.kind === "path") {
93
+ validateFieldExists(right);
94
+ }
95
+ }
96
+ /**
97
+ * 入れ子の量指定子パターンを簡易検出して ReDoS リスクを判定する。
98
+ * `(a+)+`, `(a*)*`, `(a+)*` のように、量指定子を含むグループに
99
+ * さらに量指定子が付くケースを検出する。
100
+ */
101
+ function isRedosRisk(pattern) {
102
+ // 量指定子(+, *, ?, {n,m})で終わるグループの直後に量指定子が来るパターン
103
+ return /(\+|\*|\?|\})\)(\+|\*|\?|\{)/.test(pattern);
104
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PipelineController = void 0;
4
+ const compile_filter_1 = require("../filter/compile-filter");
5
+ const compile_template_1 = require("../template/compile-template");
6
+ /**
7
+ * FilterTemplatePipeline の状態を管理する controller。
8
+ * REPL はこの API 経由でのみ pipeline を変更する。
9
+ * getPipeline() は同一オブジェクト参照を返すため、
10
+ * message-router 側に渡した pipeline と常に同期する。
11
+ */
12
+ class PipelineController {
13
+ _pipeline;
14
+ _filterExpr = null;
15
+ _templateExpr = null;
16
+ _focusExpr = null;
17
+ constructor() {
18
+ this._pipeline = { filter: null, template: null, focus: null };
19
+ }
20
+ /** 同一オブジェクト参照を返す。router 側と共有される。 */
21
+ getPipeline() {
22
+ return this._pipeline;
23
+ }
24
+ // --- Filter ---
25
+ getFilterExpr() { return this._filterExpr; }
26
+ /** フィルタ式をコンパイルして設定する。無効な式の場合は例外を投げる。 */
27
+ setFilter(expr) {
28
+ const predicate = (0, compile_filter_1.compileFilter)(expr);
29
+ this._pipeline.filter = predicate;
30
+ this._filterExpr = expr;
31
+ }
32
+ clearFilter() {
33
+ this._pipeline.filter = null;
34
+ this._filterExpr = null;
35
+ }
36
+ // --- Template ---
37
+ getTemplateExpr() { return this._templateExpr; }
38
+ /** テンプレート式をコンパイルして設定する。 */
39
+ setTemplate(expr) {
40
+ const renderer = (0, compile_template_1.compileTemplate)(expr);
41
+ this._pipeline.template = renderer;
42
+ this._templateExpr = expr;
43
+ }
44
+ clearTemplate() {
45
+ this._pipeline.template = null;
46
+ this._templateExpr = null;
47
+ }
48
+ // --- Focus ---
49
+ getFocusExpr() { return this._focusExpr; }
50
+ /** フォーカス式をコンパイルして設定する。無効な式の場合は例外を投げる。 */
51
+ setFocus(expr) {
52
+ const predicate = (0, compile_filter_1.compileFilter)(expr);
53
+ this._pipeline.focus = predicate;
54
+ this._focusExpr = expr;
55
+ }
56
+ clearFocus() {
57
+ this._pipeline.focus = null;
58
+ this._focusExpr = null;
59
+ }
60
+ // --- Factory ---
61
+ /** 式文字列から PipelineController を構築する。null/undefined はスキップ。 */
62
+ static fromExpressions(opts) {
63
+ const ctrl = new PipelineController();
64
+ if (opts.filter != null)
65
+ ctrl.setFilter(opts.filter);
66
+ if (opts.template != null)
67
+ ctrl.setTemplate(opts.template);
68
+ if (opts.focus != null)
69
+ ctrl.setFocus(opts.focus);
70
+ return ctrl;
71
+ }
72
+ }
73
+ exports.PipelineController = PipelineController;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldDisplay = shouldDisplay;
4
+ exports.renderTemplate = renderTemplate;
5
+ /** PresentationEvent にフィルタを適用する。true = 表示、false = 非表示 */
6
+ function shouldDisplay(event, pipeline) {
7
+ if (pipeline.filter == null)
8
+ return true;
9
+ return pipeline.filter(event);
10
+ }
11
+ /** テンプレートが設定されていれば1行に変換する。null = テンプレートなし */
12
+ function renderTemplate(event, pipeline) {
13
+ if (pipeline.template == null)
14
+ return null;
15
+ return pipeline.template(event);
16
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * engine → ui の逆方向依存を解消するための表示コールバックインターフェース。
4
+ * engine 層はこのインターフェースを通じてのみ表示を行う。
5
+ * 実装は ui 層の display-adapter.ts で提供される。
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,53 +1,22 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.createMessageHandler = createMessageHandler;
37
- const telegram_parser_1 = require("../../dmdata/telegram-parser");
38
- const volcano_parser_1 = require("../../dmdata/volcano-parser");
39
- const formatter_1 = require("../../ui/formatter");
40
- const eew_formatter_1 = require("../../ui/eew-formatter");
41
- const earthquake_formatter_1 = require("../../ui/earthquake-formatter");
42
- const volcano_formatter_1 = require("../../ui/volcano-formatter");
43
- const volcano_vfvo53_aggregator_1 = require("./volcano-vfvo53-aggregator");
7
+ const chalk_1 = __importDefault(require("chalk"));
44
8
  const eew_tracker_1 = require("../eew/eew-tracker");
45
9
  const eew_logger_1 = require("../eew/eew-logger");
46
10
  const notifier_1 = require("../notification/notifier");
47
11
  const tsunami_state_1 = require("./tsunami-state");
48
12
  const volcano_state_1 = require("./volcano-state");
49
- const volcano_presentation_1 = require("../notification/volcano-presentation");
50
- const log = __importStar(require("../../logger"));
13
+ const telegram_stats_1 = require("./telegram-stats");
14
+ const summary_tracker_1 = require("./summary-tracker");
15
+ const process_message_1 = require("../presentation/processors/process-message");
16
+ const to_presentation_event_1 = require("../presentation/events/to-presentation-event");
17
+ const pipeline_1 = require("../filter-template/pipeline");
18
+ const diff_store_1 = require("../presentation/diff-store");
19
+ const volcano_route_handler_1 = require("./volcano-route-handler");
51
20
  /**
52
21
  * classification と head.type から処理ルートを判定する。
53
22
  * ルーティング優先順位:
@@ -86,169 +55,130 @@ function classifyMessage(classification, headType) {
86
55
  }
87
56
  return "raw";
88
57
  }
89
- // ── ルート別処理 ──
90
- /** EEW パス: パース → 重複判定 → ログ → 表示 → 通知 */
91
- function handleEew(msg, eewTracker, eewLogger, notifier) {
92
- const eewInfo = (0, telegram_parser_1.parseEewTelegram)(msg);
93
- if (!eewInfo) {
94
- (0, formatter_1.displayRawHeader)(msg);
95
- return;
96
- }
97
- const result = eewTracker.update(eewInfo);
98
- if (result.isDuplicate) {
99
- log.debug(`EEW 重複報スキップ: EventID=${eewInfo.eventId} 第${eewInfo.serial}報`);
100
- return;
101
- }
102
- // ログ記録 (非重複報のみ)
103
- eewLogger.logReport(eewInfo, result);
104
- // 取消報の場合はログを閉じる
105
- if (result.isCancelled && eewInfo.eventId) {
106
- eewLogger.closeEvent(eewInfo.eventId, "取消");
107
- }
108
- // 最終報の場合はログを閉じ、トラッカーのイベントを終了扱いにする
109
- if (eewInfo.nextAdvisory && eewInfo.eventId && !result.isCancelled) {
110
- eewLogger.closeEvent(eewInfo.eventId, "最終報");
111
- eewTracker.finalizeEvent(eewInfo.eventId);
112
- }
113
- (0, eew_formatter_1.displayEewInfo)(eewInfo, {
114
- activeCount: result.activeCount,
115
- diff: result.diff,
116
- colorIndex: result.colorIndex,
117
- });
118
- notifier.notifyEew(eewInfo, result);
119
- }
120
- /** テキスト系 (VXSE56/VXSE60/VZSE40) パス */
121
- function handleSeismicText(msg, notifier) {
122
- const textInfo = (0, telegram_parser_1.parseSeismicTextTelegram)(msg);
123
- if (textInfo) {
124
- (0, earthquake_formatter_1.displaySeismicTextInfo)(textInfo);
125
- notifier.notifySeismicText(textInfo);
126
- }
127
- else {
128
- (0, formatter_1.displayRawHeader)(msg);
129
- }
130
- }
131
- /** 長周期地震動観測情報 (VXSE62) パス */
132
- function handleLgObservation(msg, notifier) {
133
- const lgInfo = (0, telegram_parser_1.parseLgObservationTelegram)(msg);
134
- if (lgInfo) {
135
- (0, earthquake_formatter_1.displayLgObservationInfo)(lgInfo);
136
- notifier.notifyLgObservation(lgInfo);
137
- }
138
- else {
139
- (0, formatter_1.displayRawHeader)(msg);
140
- }
141
- }
142
- /** 地震情報 (VXSE51/52/53/61 等) パス */
143
- function handleEarthquake(msg, notifier) {
144
- const eqInfo = (0, telegram_parser_1.parseEarthquakeTelegram)(msg);
145
- if (eqInfo) {
146
- (0, earthquake_formatter_1.displayEarthquakeInfo)(eqInfo);
147
- notifier.notifyEarthquake(eqInfo);
148
- }
149
- else {
150
- (0, formatter_1.displayRawHeader)(msg);
151
- }
152
- }
153
- /** 津波情報 (VTSE41/51/52) パス */
154
- function handleTsunami(msg, notifier, tsunamiState) {
155
- const tsunamiInfo = (0, telegram_parser_1.parseTsunamiTelegram)(msg);
156
- if (tsunamiInfo) {
157
- // VTSE41 (津波警報・注意報) の場合のみ状態を更新
158
- if (msg.head.type === "VTSE41") {
159
- tsunamiState.update(tsunamiInfo);
160
- }
161
- (0, earthquake_formatter_1.displayTsunamiInfo)(tsunamiInfo);
162
- notifier.notifyTsunami(tsunamiInfo);
163
- }
164
- else {
165
- (0, formatter_1.displayRawHeader)(msg);
58
+ // ── dispatch helpers ──
59
+ /** 通知のみ実行 (filter 非適用) */
60
+ function dispatchNotify(outcome, notifier) {
61
+ switch (outcome.domain) {
62
+ case "eew":
63
+ notifier.notifyEew(outcome.parsed, outcome.eewResult);
64
+ break;
65
+ case "earthquake":
66
+ notifier.notifyEarthquake(outcome.parsed);
67
+ break;
68
+ case "seismicText":
69
+ notifier.notifySeismicText(outcome.parsed);
70
+ break;
71
+ case "lgObservation":
72
+ notifier.notifyLgObservation(outcome.parsed);
73
+ break;
74
+ case "tsunami":
75
+ notifier.notifyTsunami(outcome.parsed);
76
+ break;
77
+ case "nankaiTrough":
78
+ notifier.notifyNankaiTrough(outcome.parsed);
79
+ break;
80
+ // raw: 通知なし
81
+ // volcano: VolcanoRouteHandler が通知を担当
166
82
  }
167
83
  }
168
- /** 南海トラフ関連 (VYSE50/51/52/60) パス */
169
- function handleNankaiTrough(msg, notifier) {
170
- const nankaiInfo = (0, telegram_parser_1.parseNankaiTroughTelegram)(msg);
171
- if (nankaiInfo) {
172
- (0, earthquake_formatter_1.displayNankaiTroughInfo)(nankaiInfo);
173
- notifier.notifyNankaiTrough(nankaiInfo);
174
- }
175
- else {
176
- (0, formatter_1.displayRawHeader)(msg);
177
- }
178
- }
179
- /** 火山情報パス (aggregator 経由) */
180
- function handleVolcano(msg, vfvo53Aggregator) {
181
- const volcanoInfo = (0, volcano_parser_1.parseVolcanoTelegram)(msg);
182
- if (volcanoInfo) {
183
- vfvo53Aggregator.handle(volcanoInfo);
184
- }
185
- else {
186
- (0, formatter_1.displayRawHeader)(msg);
84
+ /** outcome.stats に基づいて統計を記録する */
85
+ function recordStats(outcome, stats) {
86
+ if (outcome.stats.shouldRecord) {
87
+ stats.record({
88
+ headType: outcome.headType,
89
+ category: outcome.statsCategory,
90
+ eventId: outcome.stats.eventId,
91
+ });
92
+ }
93
+ if (outcome.stats.maxIntUpdate) {
94
+ const u = outcome.stats.maxIntUpdate;
95
+ stats.updateMaxInt(u.eventId, u.maxInt, u.headType);
187
96
  }
188
97
  }
189
98
  /** 受信データのハンドリング */
190
- function createMessageHandler() {
99
+ function createMessageHandler(options) {
100
+ const pipeline = options?.pipeline ?? { filter: null, template: null, focus: null };
101
+ const display = options?.display;
191
102
  const eewLogger = new eew_logger_1.EewEventLogger();
192
103
  const notifier = new notifier_1.Notifier();
193
104
  const tsunamiState = new tsunami_state_1.TsunamiStateHolder();
194
105
  const volcanoState = new volcano_state_1.VolcanoStateHolder();
106
+ const stats = new telegram_stats_1.TelegramStats();
107
+ const summaryTracker = new summary_tracker_1.SummaryWindowTracker();
108
+ const diffStore = new diff_store_1.PresentationDiffStore();
195
109
  const eewTracker = new eew_tracker_1.EewTracker({
196
110
  onCleanup: (eventId) => {
197
111
  eewLogger.closeEvent(eventId, "タイムアウト");
198
112
  },
199
113
  });
200
- // VFVO53 バッチ集約器
201
- const vfvo53Aggregator = new volcano_vfvo53_aggregator_1.VolcanoVfvo53Aggregator(
202
- // emitSingle: 従来の単発処理パイプライン (opts?.notify === false なら通知スキップ)
203
- (info, opts) => {
204
- const presentation = (0, volcano_presentation_1.resolveVolcanoPresentation)(info, volcanoState);
205
- (0, volcano_formatter_1.displayVolcanoInfo)(info, presentation);
206
- volcanoState.update(info);
207
- if (opts?.notify !== false) {
208
- notifier.notifyVolcano(info, presentation);
114
+ const processDeps = {
115
+ eewTracker,
116
+ eewLogger,
117
+ tsunamiState,
118
+ volcanoState,
119
+ };
120
+ /**
121
+ * 共通の表示パイプライン処理。
122
+ * filter/diffStore/summaryTracker/focus/template/compact の6ステップを一元的に実行する。
123
+ * @returns true なら表示済み。false ならフィルタで非表示。
124
+ */
125
+ function runDisplayPipeline(outcome, displayFn) {
126
+ const rawEvent = (0, to_presentation_event_1.toPresentationEvent)(outcome);
127
+ const event = diffStore.apply(rawEvent);
128
+ const displayed = (0, pipeline_1.shouldDisplay)(event, pipeline);
129
+ summaryTracker.record(event, displayed);
130
+ if (!displayed) {
131
+ return false;
132
+ }
133
+ const isFocused = pipeline.focus == null || pipeline.focus(event);
134
+ if (!isFocused && display) {
135
+ console.log(chalk_1.default.dim(display.renderSummaryLine(event)));
136
+ return true;
137
+ }
138
+ const templateOutput = (0, pipeline_1.renderTemplate)(event, pipeline);
139
+ if (templateOutput != null) {
140
+ console.log(templateOutput);
141
+ return true;
209
142
  }
210
- },
211
- // emitBatch: バッチ専用処理
212
- (batch, opts) => {
213
- const presentation = (0, volcano_presentation_1.resolveVolcanoBatchPresentation)(batch);
214
- (0, volcano_formatter_1.displayVolcanoAshfallBatch)(batch, presentation);
215
- if (opts.notify) {
216
- notifier.notifyVolcanoBatch(batch, presentation);
143
+ if (display && display.getDisplayMode() === "compact") {
144
+ console.log(display.renderSummaryLine(event));
145
+ return true;
217
146
  }
147
+ displayFn();
148
+ return true;
149
+ }
150
+ // 火山ルートハンドラ
151
+ const volcanoHandler = new volcano_route_handler_1.VolcanoRouteHandler({
152
+ volcanoState,
153
+ notifier,
154
+ runDisplayPipeline,
155
+ display,
218
156
  });
219
157
  const handler = (msg) => {
220
158
  // XML電文でない場合はヘッダ情報のみ表示
221
159
  if (msg.format !== "xml" || !msg.head.xml) {
222
- (0, formatter_1.displayRawHeader)(msg);
160
+ display?.displayRawHeader(msg);
223
161
  return;
224
162
  }
225
163
  const route = classifyMessage(msg.classification, msg.head.type);
226
- switch (route) {
227
- case "eew":
228
- handleEew(msg, eewTracker, eewLogger, notifier);
229
- break;
230
- case "seismicText":
231
- handleSeismicText(msg, notifier);
232
- break;
233
- case "lgObservation":
234
- handleLgObservation(msg, notifier);
235
- break;
236
- case "earthquake":
237
- handleEarthquake(msg, notifier);
238
- break;
239
- case "tsunami":
240
- handleTsunami(msg, notifier, tsunamiState);
241
- break;
242
- case "nankaiTrough":
243
- handleNankaiTrough(msg, notifier);
244
- break;
245
- case "volcano":
246
- handleVolcano(msg, vfvo53Aggregator);
247
- break;
248
- case "raw":
249
- (0, formatter_1.displayRawHeader)(msg);
250
- break;
164
+ // 火山は VolcanoRouteHandler に委譲
165
+ if (route === "volcano") {
166
+ volcanoHandler.handle(msg);
167
+ stats.record({
168
+ headType: msg.head.type,
169
+ category: (0, telegram_stats_1.routeToCategory)(route),
170
+ eventId: msg.xmlReport?.head.eventId ?? null,
171
+ });
172
+ return;
173
+ }
174
+ // 火山以外: processMessage → recordStats → dispatchNotify → runDisplayPipeline
175
+ const outcome = (0, process_message_1.processMessage)(msg, route, processDeps);
176
+ if (outcome == null) {
177
+ return;
251
178
  }
179
+ recordStats(outcome, stats);
180
+ dispatchNotify(outcome, notifier);
181
+ runDisplayPipeline(outcome, () => display?.displayOutcome(outcome));
252
182
  };
253
183
  return {
254
184
  handler,
@@ -256,6 +186,8 @@ function createMessageHandler() {
256
186
  notifier,
257
187
  tsunamiState,
258
188
  volcanoState,
259
- flushAndDisposeVolcanoBuffer: () => vfvo53Aggregator.flushAndDispose(),
189
+ stats,
190
+ summaryTracker,
191
+ flushAndDisposeVolcanoBuffer: () => volcanoHandler.flushAndDispose(),
260
192
  };
261
193
  }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SummaryWindowTracker = exports.WINDOW_MINUTES = void 0;
4
+ const intensity_1 = require("../../utils/intensity");
5
+ /** リングバッファの窓幅(分) */
6
+ exports.WINDOW_MINUTES = 30;
7
+ const MINUTE_MS = 60_000;
8
+ /** 直近30分のリングバッファで受信統計を追跡する */
9
+ class SummaryWindowTracker {
10
+ buckets = [];
11
+ /** イベントを記録する */
12
+ record(event, matched, now) {
13
+ const ts = now ?? Date.now();
14
+ this.pruneOld(ts);
15
+ const bucket = this.getOrCreateBucket(ts);
16
+ bucket.received++;
17
+ if (matched) {
18
+ bucket.matched++;
19
+ }
20
+ bucket.byDomain[event.domain] = (bucket.byDomain[event.domain] ?? 0) + 1;
21
+ // maxInt 追跡 (バケット単位で記録)
22
+ if (event.maxInt != null) {
23
+ const rank = (0, intensity_1.intensityToRank)(event.maxInt);
24
+ if (rank > bucket.maxIntRank) {
25
+ bucket.maxIntRank = rank;
26
+ bucket.maxIntStr = event.maxInt;
27
+ }
28
+ }
29
+ }
30
+ /** 現在のスナップショットを取得する */
31
+ getSnapshot(now) {
32
+ const ts = now ?? Date.now();
33
+ this.pruneOld(ts);
34
+ let totalReceived = 0;
35
+ let totalMatched = 0;
36
+ const byDomain = {};
37
+ for (const bucket of this.buckets) {
38
+ totalReceived += bucket.received;
39
+ totalMatched += bucket.matched;
40
+ for (const [domain, count] of Object.entries(bucket.byDomain)) {
41
+ byDomain[domain] = (byDomain[domain] ?? 0) + (count ?? 0);
42
+ }
43
+ }
44
+ // maxInt を残存バケットから再計算 (30分窓で減衰)
45
+ let maxIntRank = 0;
46
+ let maxIntStr = null;
47
+ for (const bucket of this.buckets) {
48
+ if (bucket.maxIntRank > maxIntRank) {
49
+ maxIntRank = bucket.maxIntRank;
50
+ maxIntStr = bucket.maxIntStr;
51
+ }
52
+ }
53
+ // sparklineData: 30スロット (古い順 → 新しい順)
54
+ const sparklineData = this.buildSparklineData(ts);
55
+ return {
56
+ totalReceived,
57
+ totalMatched,
58
+ byDomain,
59
+ maxIntSeen: maxIntStr,
60
+ sparklineData,
61
+ };
62
+ }
63
+ /** 統計をクリアする */
64
+ clear() {
65
+ this.buckets = [];
66
+ }
67
+ /** 30分超の古いバケットを除去する */
68
+ pruneOld(now) {
69
+ const cutoff = this.minuteStart(now) - (exports.WINDOW_MINUTES - 1) * MINUTE_MS;
70
+ this.buckets = this.buckets.filter((b) => b.minuteStartMs >= cutoff);
71
+ }
72
+ /** 指定時刻のバケットを取得、なければ作成 */
73
+ getOrCreateBucket(now) {
74
+ const ms = this.minuteStart(now);
75
+ const existing = this.buckets.find((b) => b.minuteStartMs === ms);
76
+ if (existing)
77
+ return existing;
78
+ const bucket = {
79
+ minuteStartMs: ms,
80
+ received: 0,
81
+ matched: 0,
82
+ byDomain: {},
83
+ maxIntRank: 0,
84
+ maxIntStr: null,
85
+ };
86
+ this.buckets.push(bucket);
87
+ return bucket;
88
+ }
89
+ /** 30スロットの sparkline データを生成する (古い順) */
90
+ buildSparklineData(now) {
91
+ const currentMinuteStart = this.minuteStart(now);
92
+ const data = new Array(exports.WINDOW_MINUTES).fill(0);
93
+ for (const bucket of this.buckets) {
94
+ const slotIndex = Math.round((bucket.minuteStartMs - (currentMinuteStart - (exports.WINDOW_MINUTES - 1) * MINUTE_MS)) / MINUTE_MS);
95
+ if (slotIndex >= 0 && slotIndex < exports.WINDOW_MINUTES) {
96
+ data[slotIndex] = bucket.received;
97
+ }
98
+ }
99
+ return data;
100
+ }
101
+ /** タイムスタンプを分の開始に丸める */
102
+ minuteStart(ts) {
103
+ return Math.floor(ts / MINUTE_MS) * MINUTE_MS;
104
+ }
105
+ }
106
+ exports.SummaryWindowTracker = SummaryWindowTracker;