開発personal-notification-bots

きっかけ:朝のLINE通知に「最近の動き」を足したかった

毎朝 6 時に LINE へポートフォリオの時価を送ってくる Bot がいる。家族 4 人分の資産推移を Google スプレッドシートに記録して、その日の時価だけを Flex Message のカードに並べる素朴な仕組みだ。

今朝、通知を見ながら「数字が 1 つだけだと、上がっているのか下がっているのかが分からない」と気づいた。直近 7 営業日くらいの推移か、いっそ 30 日分のバーチャートを Bubble カードに描けないか — そう思って Claude Code に解説ページを作らせた。ローカルサーバーで HTML を立ち上げてもらい、ブラウザで眺めながら方式を比較する流れにした。

調査:LINE Bubble にバーチャートは描けるか

結論から書くと、Flex Message の Bubble だけでバーチャートを「ちゃんと」描くのは厳しいことが分かった。

Bubble の構成要素は Box / Text / Separator / Image / Button といったプリミティブだけで、SVG や Canvas を埋め込むようなチャート用パーツは存在しない。棒グラフ「風」のものを作るなら、Box の幅・色・高さを 1 本ずつ計算して横並びにするしかない。Claude Code が作った HTML モックを開いて、7 営業日・30 日・90 日のスケールで並べ替えながら確認した。

  • 7 営業日ぶんなら、1 本あたりの幅にゆとりがあって読める
  • 30 日ぶんに増やすと 1 本が細くなりすぎて、Bubble の横幅 (300px 弱) に押し込むと差がほぼ消える
  • 90 日はもう棒というより縦線で、上下動の方向すら判別できない

LIFF(LINE のミニアプリ枠)で SVG を描けばいいが、それは「通知でパッと見る」体験ではない。指でタップしてアプリを開く時点で、毎朝の通知のリズムが崩れる。

ここで方針を切り替えた。

代替案:最高値からの落差を%と日付で出すカード

ユーザー(つまり自分)から出てきた代替案がそのまま正解だった。「Google スプレッドシートに 4 人分の資産推移を全部書き溜めているのだから、そこから過去のピークを取って、現在値との差分を%で出せばいい」というもの。

Bubble カードに乗せる情報はこうなった。

  • 現在の時価(既存。これは残す)
  • 過去の最高値(ピーク額)
  • そのピークが記録された日付
  • 現在値とピークの差分(%)

「最高値からマイナス 4.8%、ピーク日は 2 週間前」と読めれば、グラフを描かなくても天井からの落差は伝わる。バーチャート 30 本の代わりに、1 行 3 項目で済む。

実装:純粋関数で peakByPerson を切り出す

リポジトリは personal-notification-bots/rakuten-portfolio-tracker/ で、エントリは update-portfolio.mjs。家族 4 人分の履歴を Google スプレッドシートから読み出し、Flex JSON を組み立てて LINE Messaging API に投げるだけのスクリプトだ。

修正箇所はざっくり 3 つ。

  1. peakByPersonBefore(history, today): 履歴配列から「今日より前の最高値とその日付」を 1 人ずつ返す純粋関数を追加
  2. buildPersonBubble: Bubble の中段に「最高値 ◯◯円 / 日付 / 差分%」の 3 行を差し込み、差分がマイナスなら赤、ゼロなら緑で色を切り替え
  3. buildPersonPortfolioNotification: 4 人分の Bubble を Carousel に並べて返す関数を、最高値情報を受け取れるように引数を増やす

ロジックは履歴の配列を reduce で走らせて最大値を拾うだけ。副作用(スプレッドシート読み出し、LINE 送信)はエントリポイント側に寄せて、計算は純粋関数に閉じ込めた。Vitest のテストは 24 本全部パス。

検証:dry-run が新コードを通らなかった話

実装が一通り済んだあと、「dry-run でログだけ確認して、よければ本番送信」という流れを想定していた。が、既存の dry-run フラグは Flex JSON の組み立て手前で console.log して終わるパスだった。新しく足した peakByPersonBefore のロジックは通らない。

そのまま「受信者は 1 名(自分)だけだし、本番送信して LINE で確認する方が早い」と判断した。実行したら 1 通届き、開いてみると 本人ぶんだけ 最高値行が出ていて、父母のカードには新しい行が見えなかった。

原因は、本人の履歴は記録が長く過去のピークが明確に取れるが、父母の履歴シートはまだ追跡を始めて間もなくて十分な日数が溜まっていなかったこと。「履歴ベースで構わないからとにかく今ある中での最高値を出してくれ」という方針に倒して、診断スクリプトで peakByPersonBefore の出力を 1 人ずつ目視確認した。

結果、ロジック自体は正しく動いており、父母のカードにも最高値行(マイナス 2.3% など)がちゃんと埋まっていた。LINE に再送して、4 人分すべてに最高値行が出ているのを確認した。

Windows タスクスケジューラの時刻ズレを直す

このリポジトリは Windows のタスクスケジューラから毎朝起動される。run-update-portfolio.cmd を叩いて、その中で update-portfolio.mjs を実行するだけ。

実行時刻が README には 06:00 と書いてあったが、実際のタスク定義は 07:00 になっていた。家族の生活リズムに合わせて 07:00 へ寄せたのを忘れて、ドキュメントだけ古いまま残っていた。テストの固定タイムスタンプ(2026-01-01 のような決め打ち)はロジック検証用なので触らず、README の「定期実行時刻」だけを 07:00 に書き換えた。

Vitest 24 本を再実行してパスを確認。これで明日朝 07:00 の定期実行から新コードが動く。

メモ:HTML と Markdown の使い分けルールも更新した

今回の調査で、ローカルに立てた解説ページは Markdown ではなく HTML で書いていた。LINE Bubble の見た目を CSS で再現したり、30 本バーの動的モックを JS で切り替えたりするのに、Markdown だと表現の幅が足りなかったからだ。

この「ビジュアル比較が主目的なら HTML、文章中心なら Markdown」というルールを、グローバル CLAUDE.md の「ドキュメント管理」セクションに昇格させた。これまでは memo/{date}/ 配下に Markdown を置く前提で書いてあったが、HTML を選んでいいケース・両方併用するケースを明文化した。今回みたいに「UI モックは HTML で検討して、結論サマリは MD で残す」というハイブリッド運用を前提にしておくと、次に同種のタスクが来たときに迷わない。

学び

  • Flex Message のカードに無理してチャートを描かない。Bubble は Box の組み合わせで作るものだから、棒グラフ風レイアウトは「数本までなら可」だが「時系列の傾向を読む用途」では破綻する
  • 数字を 1 つ並べるだけだと方向が分からない。「現在値 / 過去ピーク / 差分% / ピーク日付」の 4 点セットなら、グラフがなくても落差が伝わる
  • dry-run が新コードを通らない設計だったら、フラグを直す前に本番で 1 通投げて確認した方が早いことがある(受信者が自分 1 人なら)
  • ドキュメントの実行時刻と実際のタスク定義がズレるのは、定期実行系では定期的に起こる。今回 README を直したが、もう一段「タスクスケジューラの XML を export してリポジトリに置く」みたいな仕組みを入れてもいいかもしれない