← 一覧に戻る

kowai-operation

GitHub ↗ Python 最終push: 2026/10/16 13:10

WIP(現在進行中)

Work In Progress

このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。

現在の状況

iOS 5.2.2(FCM 版)公開完了・タグ付け済み(2026-10-16)

OneSignal 無料プランの制限で 10-04 から iOS に届いていなかったプッシュ通知を FCM(HTTP v1・トピック方式)に移し、5.2.2(ビルド15)を App Store で公開した。経緯と記録は .devnotes/FCM_MIGRATION_PLAN.md(特に 7-1a)。

  • App Store: 5.2.2 / ビルド15 / READY_FOR_SALE(申請 10-15 17:36、手動リリース、段階的リリースなし)
  • リリースタグ v5.2.2 を kowai-ios の efc0095(ビルド15 を作ったツリー)に作成・push 済み
  • サーバー: kowai-server a503b3d を配備済みで、10-15 14:43 から ios_provider=fcm。監視 checkPush(10分ごと)→ SNS kowai-push-alerts → メール
  • 5.2.2 の中身: Xcode 26 対応(GMA 12・Firebase 11.15)、OneSignal と NSE の撤去・FCM 化、Const.baseUrlString の https 化、YouTube のエラー153 の修正、最低 iOS 15.0
  • 旧版(5.2.1 以前)の端末には、5.2.2 に更新して起動するまで通知が届かない

いま見守り中:

  • 5.2.2 のクラッシュとレビュー(App Store Connect・レビュー欄)
  • 配信の異常は checkPush(10分ごと)が SNS でメールしてくる。公開後の手での確認はしない(下記)

次にやること(優先度順):

  1. nginx 80番の /khApi/ パススルー撤去の判断 — iOS 5.2.2・Android 1.0.3 とも最新版は https になった。残るのは未更新の端末からの http アクセスなので、nginx のアクセスログで量を見てから決める
  2. kowai-android の持ち越し: エッジツーエッジ(insets)対応 → Realm 10.19.0 からの脱却(Room 移行)→ 次回アップデート時に play_upload.py を拡張(bundle と symbols を1つの edit に)

未決事項 / 保留:

  • 公開後の確認(計画 7-1 の 11)と確認表の記録漏れの確認はやらない(You・10-16)。App Store 版で ios_test が外れることの確認、初回の実配信(10-18)後の記録と実機での受信の確認、「App Store 5.2.1 → 最終ビルドへの直接の上書き」と緊急停止の本番での試行の有無の確認は、いずれも見送り
  • kowai-ios/Kowai/AppDelegate.swift に TwitterKit の consumer key・secret が直書きのまま(private リポ・計画の付録)。TwitterKit は 5.2.2 でも残っている。撤去・更新するかは未定
  • Android のネイティブデバッグシンボルは次回アップデート時に同一 edit で添付する(DECISIONS 2026-09-06)。zip は ~/Downloads/kowai-native-debug-symbols-vc11.zip
  • Play アップロード用スクリプトは scratchpad にあるだけ。リポジトリに入れるか未定
  • .devnotes/ROADMAP.md の「今週」に 6 月時点の見守り項目(レビュー欄のクラッシュ報告、kowai-server の稼働観察)が残っている。棚卸しが必要(今回は FCM 移行で片付いた項目だけ更新した)

過去のWIPアーカイブ

(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)

2026-10-16 12:40 時点のスナップショット

Android v1.0.3 リリース完了・タグ付け済み(2026-09-06)

targetSdk 36 対応(Play の 2026-08-31 期限)を含む v1.0.3 / versionCode 11 が審査を通過し、製品版で配信中。期限には余裕を持って着地した。

  • Play の状態: [production] versionCode 11 / status=completed / 1.0.3
  • リリースタグ v1.0.3 を kowai-android の 4060c7d(出荷 AAB をビルドしたツリー)に作成・push 済み
  • Gradle 8.11.1 / AGP 8.9.1 / compileSdk・targetSdk 36 / minSdk 24
  • Realm 10.19.0 は AGP 8.9.1 でも動作した

次にやること(優先度順):

  1. エッジツーエッジ(insets)対応 — ボトムナビのラベルがジェスチャーピルに潰れる / ヘッダーがステータスバーに潜る / 詳細画面のタイトルと共有ボタンがステータスバーと衝突 / 広告バナーにピルが重なる。targetSdk 35 の時点から現行製品版に入っている既存不具合で、36 化とは無関係。作業量は1〜2時間の見込み
  2. Realm 10.19.0 からの脱却(Room 移行) — 今回は AGP 8.9.1 で通ったが、毎年の AGP 更新でいつ落ちてもおかしくない。落ちた時点でアップデートが出せなくなる=Play から実質退場。使用範囲は 78 kt 中 16 ファイル(うち14が database/ パッケージ・計578行)で、外に漏れているのは KowaiApplication.kt と MainActivity.kt のみ
  3. 次回アップデート時に play_upload.py を拡張 — bundle と symbols を1つの edit にまとめて投げるようにする。これでネイティブデバッグシンボルが API だけで完結する
  4. 次の iOS リリースで Const.swift の baseUrlString を https に → 全クライアント https 化後、nginx 80番の /khApi/ パススルーを撤去

未決事項 / 保留:

  • ネイティブデバッグシンボルは vc11 への後付けを見送り、次回アップデート時に同一 edit で添付する方針に変更(DECISIONS 2026-09-06)。zip は ~/Downloads/kowai-native-debug-symbols-vc11.zip に保管。Realm 10.19.0 のままなら .so が同一バイト列なので vc12 でも使い回せる
  • Play アップロード用スクリプト(Python / Play Developer API 直叩き)は現在 scratchpad にあるだけ。リポジトリに入れるか未定(3 の作業と併せて判断)
  • .devnotes/ROADMAP.md の「今週」に 6 月時点の見守り項目(レビュー欄のクラッシュ報告、kowai-server の稼働観察)が残っている。棚卸しが必要

2026-08-29 14:27 時点のスナップショット

targetSdk 36 対応リリース完了・製品版公開済み(2026-08-29)

Google Play の対象 API レベル要件(期限 2026-08-31)に対応し、kowai-android の versionCode 11 / versionName 1.0.3 を製品版まで公開した。期限には3日の余裕を持って着地。

  • Gradle 8.9 → 8.11.1 / AGP 8.6.1 → 8.9.1 / compileSdk・targetSdk 35 → 36
  • Realm 10.19.0 は AGP 8.9.1 でも問題なく動作した(今回最大のリスクが空振り)
  • 16KB ページサイズ対応(librealm-jni.so 4 ABI すべて LOAD align=0x4000)は維持
  • API 36 エミュレータで targetSdk 35 / 36 を A/B 検証 → 回帰ゼロ
  • 積み残しも同梱: is_info の型耐性強化、API 3本 + プライバシーポリシー URL の https 統一
  • コミット: 4060c7d(本体)/ f1d2977(デバッグシンボル調査の記録)

いま見守り中:

  • 製品版 versionCode 11 の審査・配信状況とクラッシュ率(Play Console / レビュー欄)
  • Android 16 実機ユーザーからのエッジツーエッジ絡みの表示崩れ報告

次にやること(優先度順):

  1. エッジツーエッジ(insets)対応 — ボトムナビのラベルがジェスチャーピルに潰れる / ヘッダーがステータスバーに潜る / 詳細画面のタイトルと共有ボタンがステータスバーと衝突 / 広告バナーにピルが重なる。targetSdk 35 の時点から現行製品版に入っている既存不具合で、36 化とは無関係。作業量は1〜2時間の見込み
  2. Realm 10.19.0 からの脱却(Room 移行) — 今回は通ったが、毎年の AGP 更新でいつ落ちてもおかしくない。落ちた時点でアップデートが出せなくなる=Play から実質退場。使用範囲は 78 kt 中 16 ファイル(うち14が database/ パッケージ・計578行)で、外に漏れているのは KowaiApplication.kt と MainActivity.kt のみ。既読フラグ 3 モデルだけ一回限りの移行コードが必要で、他はサーバーから再取得できるキャッシュ
  3. ネイティブデバッグシンボル zip を vc11 にアップロード(Play Console の画面から。API では後付け不可)
  4. 次の iOS リリースで Const.swift の baseUrlString を https に → 全クライアント https 化後、nginx 80番の /khApi/ パススルーを撤去

未決事項 / 保留:

  • Play アップロード用スクリプト(Python / Play Developer API 直叩き)は現在 scratchpad にあるだけ。リポジトリに入れるか未定
  • ネイティブデバッグシンボルの zip は作成済み(~/Downloads/kowai-native-debug-symbols-vc11.zip・配信中 AAB の .so と sha256 一致確認済み)。Play Console → アプリバンドル エクスプローラ → vc11 → ダウンロード タブ から手動アップロードするだけ(未実施)
  • .devnotes/ROADMAP.md の「今週」に 6 月時点の見守り項目が残っている。棚卸しが必要

2026-06-09 18:49 時点のスナップショット

Android クラッシュ修正完了(2026-06-09)

5/24 の Lightsail 移管以降に発生していた Android クラッシュ(Enter ボタン押下・詳細ページ遷移)を緊急修正。エミュレーターでの検証で OK 確認。

  • 原因 1: nginx の HTTPS 強制が、旧アプリの http://kh.thomsonsapp.com ハードコードと衝突 → /khApi/* のみ HTTP パススルーに戻して救済
  • 原因 2: is_html / is_info を str に変えていたのが Android Realm モデル Boolean 定義と不整合 → bool に戻し、S3 上の既存 4189 件も bool 版で全件再 put

いま見守り中:

  • Play Console / レビュー欄でクラッシュ報告が収まるか(5/29 / 5/31 のレビュー以降)
  • OneSignal 予約配信が予定時刻に届くか
  • cron / S3 同期が継続するか

次にやること(任意・優先度低め):

  • Route53 の子 hosted zone kh.thomsonsapp.com の削除(親に一本化済み・月 $0.50 節約)
  • 旧 Cake (fn-* EC2) の KhApi コントローラ・関連コード・vhost のクリーンアップ
  • 次の iOS / Android リリースで baseURL を https:// に統一 → 旧 80 番 /khApi/ パススルーを撤去
  • 次の Android リリースで MainActivity.kt:232 の getBoolean(2) を getInt(2) != 0 に変更(サーバー側型変更への耐性向上)

ペンディング(中期):

  • sendPushNotification 配信実績(errored 件数)の検知・アラート
  • OneSignal SDK の更新検討
  • kowai-android: Realm 10.19.0 からの脱却(MongoDB が EOL 化)

2026-06-09 18:00 時点のスナップショット

kowai サーバー機能の独立 Lightsail 移管が完了(2026-05-24)

旧 Cake (fn-* EC2、footballnext と同居) で動いていた /khApi/* と /fearstoryPortal/* 一式を、新 Lightsail インスタンス kowai-server に独立移管した。Codex レビュー(2 ラウンド)反映、DNS 切替、HTTPS 化まですべて完了済み。

  • 本番URL: https://kh.thomsonsapp.com
  • Lightsail: 54.249.174.108 / Ubuntu 24.04 / Python 3.12 / 1GB
  • DB は既存 RDS (kowai schema) を共用、スキーマ無変更
  • 旧 fn-* EC2 への kowai 関連トラフィックは無し(footballnext は継続使用)
  • 詳細: .devnotes/lightsail-migration-report.md と .gods-talk/lightsail-migration-review-{001,002}-{issue,feedback}.md

次にやること(任意・優先度低め):

  • Route53 の子 hosted zone kh.thomsonsapp.com の削除(親に一本化済み・月 $0.50 節約)
  • 旧 Cake (fn-* EC2) の KhApi コントローラ・関連コード・kh.thomsonsapp.com vhost のクリーンアップ
  • 数日〜1週間の稼働観察(OneSignal 予約配信が予定時刻に届くか、S3 同期が継続するか)

ペンディング(中期):

  • sendPushNotification 配信実績(errored 件数)の検知・アラート
  • OneSignal SDK の更新検討
  • kowai-android: Realm 10.19.0 からの脱却(MongoDB が EOL 化)
  • kowai-android: 古い androidx 群(appcompat 1.0.2 等)の近代化

2026-05-23 19:00 時点のスナップショット

kowai-android の Google Play 16KBページサイズ対応(対応期限 2026-05-31)

技術作業はすべて完了。署名済みリリースAAB(versionCode 10 / versionName 1.0.2)をビルド済み。

  • 成果物: kowai-android/app/build/outputs/bundle/release/app-release.aab
  • 内容: 16KB対応 / targetSdk 35 / nend SDK削除 / AdMob 25.2.0・ADG 2.36.0・Realm 10.19.0 / 初回タップ無反応バグ修正
  • 16KBアラインメント・タップ修正とも検証済み(実機 YAL-L21)

次にやること:

  • vc10 のAABを Play Console にアップロードして公開(先にアップした vc9 は破棄)
  • アップロード時の3警告(minSdk低下 / 難読化解除ファイルなし / ネイティブシンボルなし)はすべて無視可

ビルド環境メモ:

  • JAVA_HOME は Android Studio 同梱 JBR(/Applications/Android Studio.app/Contents/jbr/Contents/Home)
  • 署名は kowai-android/keystore.properties(gitignore対象・要パスワード記入)から読む
  • リリースAAB生成: cd kowai-android && ./gradlew :app:bundleRelease

未決・改善余地:

  • Realm 10.19.0 は deprecated(MongoDB が Realm を EOL化)。中期的に脱却検討
  • 古い androidx 群(appcompat 1.0.2 等・2018年版)は今回未更新。Android 15 実機での表示崩れがあれば個別対応
  • loadPieceItem の onFailure が握り潰し・タップ時フィードバックなし(今回のタップ修正とは別の改善余地)

2026-05-21 18:27 時点のスナップショット

iOS プッシュ通知の不達は復旧済み(2026-05-21)。

  • 原因: OneSignal に登録された APNs 証明書の Bundle ID が jp.thomsons.ThomsonK002(誤)で、実アプリ jp.thomsons.Thomson002 と不一致 → APNs が全 push を拒否(errored)。4月の証明書更新時の打ち間違いが原因
  • 対処: 正しい Bundle ID の証明書に更新済み。自分の端末へのテスト送信で successful:1・実機着信を確認
  • リポジトリ構成: kowai-operation 配下の kowai-ios / kowai-android / footballnext-server は .gitignore で分離(各リポは独立して扱う)

次にやること:

  • 次の怖い話追加時に管理画面から本番送信し、約1万台への配信を実地確認
  • Apple Developer の誤 App ID jp.thomsons.ThomsonK002 を削除

未決・改善余地:

  • sendPushNotification() が errored を検知できない(送信後に配信実績を見る監視の追加余地)
  • OneSignal のタグ上限(プラン上限2 / アプリは3タグ送信)の見直し

ROADMAP(計画)

ロードマップ

今週

  • kowai-android: vc10 のリリースAABを Play Console にアップロード・公開(16KB対応期限 2026-05-31)
  • kowai サーバー機能を独立 Lightsail (FastAPI) に移管・DNS 切替・HTTPS 化
  • Android クラッシュ(Enter / 詳細ページ)緊急修正 + S3 全件 bool 化
  • kowai-android: targetSdk 36 対応(Play 期限 2026-08-31)— vc11 / 1.0.3 を製品版公開
  • kowai-android: リリースタグ v1.0.3 を作成(初のタグ。v<versionName> 形式を規約として採用)
  • kowai-android: 次回アップデート時に play_upload.py を拡張し、bundle と symbols を1つの edit にまとめて投げる(vc11 への後付けは見送り。同一 edit 内なら API で添付でき手動操作が不要になる。zip は作成済みで .so 同一なら使い回し可)
  • kowai-android: エッジツーエッジ(insets)対応 — ボトムナビのラベルがジェスチャーピルに潰れる / ヘッダーがステータスバーに潜る / 詳細画面のタイトル・共有ボタンの衝突 / 広告バナーへのピル重なり(targetSdk 35 時点からの既存不具合・1〜2時間)
  • レビュー欄でクラッシュ報告が収まるか見守り
  • kowai-server の数日稼働観察(OneSignal 予約配信が予定時刻に届くか、S3 同期が継続するか)
  • Apple Developer の誤 App ID jp.thomsons.ThomsonK002 を削除
  • kowai-ios: プッシュ通知を OneSignal → FCM(HTTP v1・トピック方式)に移行し、5.2.2(ビルド15)を App Store 公開(2026-10-16 確認。経緯は FCM_MIGRATION_PLAN.md)
  • kowai-ios: リリースタグ v5.2.2 を作成(efc0095。v<versionName> 形式)
  • FCM 公開後の確認(計画 7-1 の 11): App Store 版 5.2.2 に更新した端末で ios_test が外れること / 初回の実配信(10-18 日曜夜の全配信)の後に kh_push_logs と実機での受信(やらないことにした・You 10-16。異常は checkPush の警告メールで拾う)

今月

  • Route53 の子 hosted zone kh.thomsonsapp.com を削除(親に一本化済み・月 $0.50 節約)
  • 旧 Cake (fn-* EC2) の KhApi コントローラ・関連コード・kh.thomsonsapp.com vhost をクリーンアップ(fn-* EC2 自体は footballnext で継続)
  • sendPushNotification() に配信実績(errored 件数)の検知・アラートを追加(FCM 移行で kh_push_logs + checkPush(10分ごと)+ SNS メールとして実装)
  • OneSignal のタグ上限(プラン上限2 / アプリは3タグ送信)の見直し — movie 通知のターゲティング影響を確認(FCM 移行で OneSignal を撤去したため不要に)
  • Play アップロードスクリプト(Play Developer API 直叩き・現在 scratchpad のみ)をリポジトリに格納するか判断
  • API レスポンス型変更時のクロスチェック手順をチェックリスト化(クライアント側 Realm / Swift モデル定義の grep を必須に)

今四半期

  • 次の iOS リリース: kowai-ios/Kowai/Const.swift:14 の baseUrlString を https:// に変更(5.2.2 で完了)
  • 次の Android リリース: HomeArticleViewModel.kt / HomeMovieViewModel.kt / StoryListViewModel.kt:33 の http:// を https:// に、MainActivity.kt:232 の型耐性を強化(vc11 で完了。ただし getInt(2) != 0 は不採用 — サーバーが bool を返すため JSONException になる。bool / int / string の全型を受ける形にした。DECISIONS 2026-08-28 参照)
  • 全クライアントが HTTPS 化したら nginx の 80 番 /khApi/ パススルーを撤去して HTTPS only に戻す(最新版は iOS 5.2.2・Android 1.0.3 とも https。残るのは未更新端末の http アクセスなので、nginx のアクセスログで量を見て判断)
  • kowai-android: Realm 10.19.0 からの脱却(Room 移行)— 「いつか」から前倒し。今回 AGP 8.9.1 では通ったが、毎年の targetSdk 更新に伴う AGP 更新でいつ落ちてもおかしくなく、落ちた時点でアップデートが出せなくなる。使用範囲は 78 kt 中 16 ファイル(うち14が database/・計578行)、外部は KowaiApplication.kt と MainActivity.kt のみ。既読フラグ 3 モデルだけ一回限りの移行コードが必要
  • kowai-server の S3 出力を ACL=public-read → bucket policy ベース / CloudFront 経由に移行(Block Public Access 適合化)
  • kowai-server の OneSignal 失敗時 candidate 戻し or 手動再試行 UI(FCM 移行で再送(管理者コマンド・管理画面の「再送」)として実装)

いつか

  • OneSignal SDK の更新検討(iOS は現状 2.12.6 / 最新は 5.x 系)(FCM 移行で OneSignal を撤去したため不要に)
  • kowai-android: 古い androidx 群(appcompat 1.0.2 等・2018年版)の近代化
  • kowai-android: loadPieceItem のエラーハンドリング改善(onFailure の握り潰し・タップ時フィードバックなし)
  • kowai-server の RSS クローラ強化(旧 RssAnalytics 互換: data-lazy-src / 相対 URL 絶対化 / URL リダイレクト解決)
  • kowai-server の Cake 互換性 golden test(API / S3 JSON の代表 fixture を Cake 出力と diff)

DECISIONS(意思決定)

意思決定記録

このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。


2026-09-06: ネイティブデバッグシンボルは vc11 に後付けせず、次回アップデート時に同一 edit で添付する

背景: vc11 用のシンボル zip は作成済み(配信中 AAB の .so と sha256 一致確認済み)で、Play Console の画面から後付けアップロードできる状態にあったが未実施のまま残っていた。

決定: vc11 への後付けは行わない。次回アップデート(vc12 以降)のアップロード時に、bundle と同じ edit の中で deobfuscationFiles/nativeCode を投げて添付する。

理由: ① vc11 への後付けは Play Console の手動操作が必要で、その一回のためだけに画面を触ることになる ② 次回は bundle・symbols を1つの edit にまとめれば API で完全自動化できる(後付けが不可なだけで、同一 edit 内なら API で添付可能)。play_upload.py を拡張すれば以降のリリースで恒久的に解決する ③ 現時点でネイティブクラッシュが実際に発生しているわけではなく、緊急性がない ④ zip は Realm 10.19.0 が変わらない限り .so が同一バイト列なので、vc12 でもそのまま使い回せる(作り直し不要)

2026-08-29: ネイティブデバッグシンボルは Play Console の画面から後付けする(同日の前項を訂正)

背景: 同日のひとつ前の決定で「Realm を使う限りネイティブデバッグシンボルは解消不能」と結論したが、これは誤りだった。.devnotes を expert-brains に吸収する過程で、product-ops/knowledge/google-play-annual-compliance.md に反証が既に記録されていることが判明した。

決定: 配信中の AAB から base/lib/<abi>/librealm-jni.so を取り出して <abi>/ 構成の zip に固め、Play Console → アプリバンドル エクスプローラ → 該当バージョン → ダウンロード タブから versionCode 11 に手動アップロードする。

理由: ① 後付けができないのは Play Developer API だけで、Play Console の画面からは可能。前項で「UI でも同じ」と書いたのは検証せずに断定した誤り ② .symtab が strip されていても .dynsym が残っていれば関数名まで解決できる。 実測で librealm-jni.so の .dynsym は 9191 エントリあり、「strip 済み=シンボルなし」は早合点だった ③ 作成済み zip 内の .so 4種は配信中 AAB の .so と sha256 が完全一致することを確認済みで、そのまま versionCode 11 に使える。

教訓: 「できない」と結論する前に expert-brains の該当 knowledge を先に引くべきだった。今回は wrap-up の吸収工程まで気付かなかった。

2026-08-29: ネイティブデバッグシンボルは Realm を使う限り解消不能と結論し、debugSymbolLevel は no-op のまま残す

背景: 製品版昇格後、Play Console に「ネイティブデバッグシンボルがアップロードされていません」警告が出た。ネイティブクラッシュが生アドレスでしか読めなくなる。

決定: app/build.gradle の debugSymbolLevel = 'SYMBOL_TABLE' を有効化した上で、現状は no-op であることを長いコメントで明記して残す。警告の解消は諦める。

理由: ① AGP がシンボルを抽出するのはプロジェクト自身がビルドしたネイティブライブラリのみで、AAR 依存の既製 .so は対象外 ② 唯一のネイティブコードである librealm-jni.so がストリップ済み(.symtab / .debug_info なし、.dynsym のみ)で、Realm は開発終了済みのためシンボル入り成果物も配布していない(Maven Central の 10.19.0 配下は aar / sources / javadoc のみ)③ 公開済み versionCode 11 への後付けアップロードも API が FAILED_PRECONDITION で拒否。AAB はバンドルと同じ edit 内でしかシンボルを添付できない。代替案として「.so をそのまま zip にして上げる」も検討したが、得られるのは .dynsym(エクスポート関数名)だけで Realm 内部の関数は解決せず、手間の割に見返りが薄いので見送り。設定自体は無害なので、Realm 脱却後や自前ネイティブコード追加時に自動的に効くよう残す。

2026-08-28: is_info は getInt(2) != 0 ではなく bool / int / string の全型を受ける形にする

背景: ROADMAP と WIP に「次の Android リリースで MainActivity.kt:232 の getBoolean(2) を getInt(2) != 0 に変更(サーバー側型変更への耐性向上)」と書いていたが、実装前にサーバーの実データを確認したところ all_story_contents_ids[i][2] は全 4291 件が bool だった。

決定: getInt(2) != 0 は採用せず、when (val v = contentInfo.get(2)) で Boolean / Number / String のいずれでも壊れない形にする。

理由: org.json の JSONArray.getInt() は Boolean を渡されると JSONException を投げるため、メモどおりに実装すると 2026-06-09 に直したクラッシュを逆向きに再発させることになった。メモの意図は「サーバー側の型変更への耐性」であり、片方の型に決め打ちする限りその意図は満たせない。3型すべてを受ければどちらに振れても壊れない。

2026-08-28: targetSdk 36 対応とエッジツーエッジ修正を分離し、期限対応を優先する

背景: Play の対象 API レベル要件(2026-08-31 期限)対応にあたり、API 36 エミュレータでの検証でヘッダーがステータスバーに潜る・ボトムナビのラベルがジェスチャーピルに潰れる等の表示崩れが見つかった。

決定: targetSdk 36 化のみを versionCode 11 としてリリースし、エッジツーエッジ(insets)対応は次のリリースに回す。

理由: targetSdk 35 / 36 の A/B 検証で表示が完全に同一だったため、この崩れは 36 化による回帰ではなく、targetSdk 35 の時点から現行製品版に入っている既存不具合と判明した。エッジツーエッジの強制は Android 15(targetSdk 35)で既に掛かっており、36 の変更はオプトアウト属性の無効化のみ。したがって 36 化を止める理由にならず、期限3日前に UI へ手を入れるリスクの方が大きい。作業量は1〜2時間と見積もれるので次リリースで独立して対応する。

2026-08-28: Play へのアップロードは既存サービスアカウント + 独立スクリプトで行う(Gradle プラグインは入れない)

背景: 内部テストへの AAB アップロードを自動化するにあたり、Gradle Play Publisher プラグインの導入と、API を直接叩く独立スクリプトの2案があった。

決定: ~/.config/gcloud/play-console-sa.json(study-timer-ops 用に作成済み)を使い、Python の独立スクリプトから Play Developer API を直接叩く。build.gradle は触らない。

理由: ① そのサービスアカウントが実録!怖い話に対するリリース管理権限を既に持っていることを edits 作成で実証できた(新規の権限付与は不要)② Gradle プラグインを足すと再ビルドが必要になり、検証済みの AAB を作り直すことになる。targetSdk とビルド基盤を丸ごと更新した直後でリスクが高い ③ スクリプトなら既存の AAB をそのまま投げられる。プラグイン化は期限が片付いてから次リリースに向けて検討する

2026-06-09: 旧アプリ救済のため /khApi/* だけは HTTP 受付を残す

背景: リリース済み iOS / Android アプリが http://kh.thomsonsapp.com/khApi/... をハードコードしており、5/24 の 80→301→HTTPS 化で旧クライアントが詰まる事態に。レビュー欄に「読めなくなった」報告が発生した。

決定: nginx の 80 番 server で /khApi/* だけは HTTP のままパススルーする。/login・/fearstoryPortal・/internal 等の認証 / 副作用系は HTTPS 強制を維持。

理由: /khApi/* は読み出し専用・認証情報なしなので HTTP 公開しても攻撃面はほぼ変化なし。代替案として「iOS / Android アプリの baseURL を https に変えて再リリース」も検討したが、審査含めて即時救済にならない。次のリリース時に全クライアントが https に揃ったらこのパススルーは撤去する短期措置として位置づけ。

2026-06-09: is_html / is_info はクライアント側 Realm モデルに合わせて bool で返す

背景: Codex round1 medium-7 で「API と S3 で型を str "0"/"1" に揃える」と判断したが、Android Realm モデル StoryContent.kt:34-35 が is_html: Boolean / is_info: Boolean で受けており、realm.createOrUpdateObjectFromJson が "0" を Boolean 変換できず JSONException → クラッシュしていた。

決定: schemas.py:StoryItem / stories.py:_to_item / cron.py:_story_struct の is_html / is_info を bool に戻す。S3 上の既存 4189 件も resync_all_stories.py で bool 版に一括上書き。

理由: ① クライアントが Boolean を要求しているのが実装で確認できた事実 ② iOS の SwiftyJSON は .boolValue で int / bool / "true"/"false" 文字列いずれも柔軟に扱うため bool に統一しても影響なし ③ 今後 API レスポンス型を変更するときは、必ずクライアントの Realm / Swift モデル定義を grep してクロスチェックする運用に切り替える。

2026-05-24: kowai サーバーは独立 Lightsail (FastAPI) に移管、Cake は使わない

背景: kowai 関連の /khApi/* と /fearstoryPortal/* が footballnext-server (CakePHP, EC2) に間借りしていた。クロール / S3 同期 / 予約 push まで巻き込んで動いていて、footballnext 側の障害・改修の影響を直接受ける構造だった。

決定: 新 Lightsail インスタンス kowai-server を独立で立て、FastAPI + Jinja2 + SQLAlchemy 2.0 で全面移植する。DB は既存 RDS (kowai schema) を共用してスキーマは触らない。kh.thomsonsapp.com の DNS を新サーバーに切り替えて完全分離する。

理由: ① 言語スタックを Python に統一して保守性を上げる ② footballnext と物理的に分けて互いの障害を遮断 ③ RDS は同じスキーマで動作実績がある ④ 旧 fn-* EC2 自体は footballnext で継続使用するので即廃止コストは無し。代替案として「Cake 側に閉じこめたまま改修」も検討したが、PHP 5 系コードベースを今後も保守する負債が大きすぎるため不採用。

2026-05-24: 副作用エンドポイントは /internal/*(127.0.0.1 限定)と /fearstoryPortal/api/*(ログイン必須)に分離、/khApi/* は読み出し API のみ

背景: 移植初期は Cake 互換で /khApi/* 配下に sendPushNotification や cron 系 13 本まで全て無認証で並べていた。Codex レビュー round1 で「外部 IP から push 全配信や DB 書き換えが叩ける」と high 指摘。

決定: ルーティングを 3 系統に再構成する。

  • /khApi/*: モバイル公開読み出し API(stories / movies / articles / spots)のみ
  • /internal/*: cron 用副作用エンドポイント(13 本)+ /internal/dbhealth。nginx で allow 127.0.0.1; allow ::1; deny all;
  • /fearstoryPortal/api/*: 管理画面 JS から叩く副作用 API。require_login + CSRF dependency

理由: モバイル公開と内部処理が同じ prefix に並んでいると認証境界が曖昧になる。prefix で物理的に分ければ nginx の location ベースで防御でき、設計の意図がコードからも読める。crontab は 127.0.0.1:8000/internal/* を叩く形に変更。

2026-05-24: 時刻基準は Python 側 JST に統一、func.now() は使わない

背景: RDS の @@global.time_zone が UTC のまま動いていて、stories.py の published_date < func.now() で公開判定が 9 時間ズレていた(旧 Cake は datetime() で JST 文字列を渡しており、DB の文字列比較で完結していたため問題が表面化しなかった)。

決定: すべての時刻判定を Python ホスト時刻(Asia/Tokyo の naive datetime)に統一。MySQL の NOW() / func.now() は使わない。RDS の time_zone は触らない。

理由: ① RDS の time_zone を変えると他システムへの影響が読めない ② アプリ側で datetime.now() に揃える方が変更箇所が少ない(2 ヶ所だけ) ③ YouTube publishedAt (UTC) も datetime.fromisoformat(...).replace(tzinfo=utc).astimezone() で JST naive に変換して保存することで、DB 側は一貫して JST 文字列のままに保てる。

2026-05-24: 予約 push は atomic claim(UPDATE WHERE candidate=:val)で二重送信を防ぐ

背景: sendReservedNotification が「全 type を SELECT → 条件一致なら candidate=0 を commit → OneSignal 送信」の順だった。--workers 2 で同じ分に走ると、両 worker が同じ candidate を読み終えてから両方 0 にして両方送る race が成立する。

決定: UPDATE kh_notification_types SET candidate=0 WHERE id=:id AND candidate=:val の atomic claim にする。rowcount==1 の呼び出しだけが OneSignal 送信に進む。

理由: 重複送信は本番ユーザーに直接迷惑がかかる。advisory lock や SELECT FOR UPDATE も検討したが、MySQL の UPDATE 自体が行ロックを取るため最小実装で十分。flock を crontab 側にも入れて多段でガード。

2026-05-24: S3 ACL=public-read を維持(短期)、Block Public Access への移行は将来

背景: Cake 由来の s3:PutObject + ACL=public-read を kowai-server でも踏襲。Codex から「将来の Block Public Access / Object Ownership Bucket owner enforced と衝突する」と low 指摘。

決定: 短期は ACL=public-read を維持。将来 bucket を作り直す / Object Ownership を切り替えるタイミングで、bucket policy ベース or CloudFront 経由に移行する。

理由: モバイルアプリ側の取得 URL を変えると再リリースが必要になる。S3 → CloudFront 化はキャッシュ戦略まで含めた別案件として切り出した方が安全。

2026-05-21: kowai-android の nend SDK を完全削除する

背景: Google Play の16KBページサイズ要件対応で、ネイティブ依存SDKを16KB対応版へ更新する必要があった。nend 広告SDKは更新版が必要だが、nend は2024/3にサービス終了しており16KB対応版が存在しない。

決定: nend SDK をアプリから完全に削除する(依存・専用ファイル・レイアウト・広告ローテーション分岐すべて)。空いた広告枠は AdMob / ADG にフォールバックさせる。

理由: nend は16KB対応版が出ない上、そもそも2024/3以降は広告が配信されていない(空表示)。残す意味がなく、削除が16KB対応の前提でもある。広告比率データ(Realm)は変更せず、enum変換で UNKNOWN→既存の else 分岐に落ちる形にして影響を最小化。

2026-05-21: kowai-android の minSdk を 21→24 に引き上げる

背景: 16KB対応の ADG SDK 2.36.0 が minSdk 23 以上を要求。AdMob 等の新版も古いAndroidを切り捨てる方向。

決定: minSdk を 21→24(Android 7.0)に引き上げる。

理由: API 21〜23(Android 5.0〜6.0)は2026年時点で利用シェアがほぼゼロ。前回更新が2023年で長期未更新だったこともあり実害は小さい。tools:overrideLibrary での強制回避は実行時クラッシュのリスクがあり不採用。24 にしておけば新しい広告SDK群の要求も満たせる。

2026-05-21: Play への提出形式を APK→AAB に移行する

背景: 前回リリース(2023)は APK 形式。16KB対応リリースにあたり提出形式を確認した。

決定: AAB 形式で提出する。

理由: 調査の結果、このアプリは既に Play App Signing 登録済み(配信版が Google署名鍵で署名されている)と判明。kowai 鍵はアップロード鍵として機能し、AAB 提出に追加手続きは不要だった。AAB は今後の標準形式。

2026-05-21: 多重タップガードの窓を 3.5秒→0.6秒に短縮する

背景: 一覧で項目をタップしても初回が無反応で再タップが必要、という既存バグを logcat で調査。原因は BasicUtil.isDisableClickEvent() の3.5秒グローバル多重タップガードが、通常のテンポの操作まで全部ブロックしていたこと(10タップ中2遷移を確認)。

決定: ガードの窓を 3500ms→600ms に短縮(定数 CLICK_GUARD_MS として明示)。

理由: ガード自体(誤連打で詳細画面が二重に開くのを防ぐ)は残す価値がある。誤連打は200ms程度で発生するため600msで十分防げ、かつ通常の閲覧操作はブロックしない。ガード撤廃(0秒)は二重遷移リスクが残るため不採用。

2026-05-21: 古い androidx 群は今回まとめて更新しない

背景: appcompat 1.0.2 など2018年版の androidx 依存が多数。compileSdk 35 化にあたり一括近代化も検討した。

決定: 16KB対応に必要な変更のみに留め、古い androidx 群の一括アップグレードは今回行わない。

理由: compileSdk/targetSdk 35 でビルドは通っており、大量アップグレードはテーマ・リソース属性・Navigation API 等の広範な破壊を招くリスクが高い。5/31期限を優先し変更範囲を限定。Android 15 実機での表示崩れがあれば個別対応する。

2026-05-21: ネストした独立リポは gitignore で分離する

背景: kowai-operation(運用リポ)配下に、独立した git リポジトリである kowai-ios / kowai-android / footballnext-server をクローンして配置した。このままだと親リポが各ディレクトリの中身を gitlink として中途半端に記録してしまう。

決定: 3リポを kowai-operation の .gitignore に追加して追跡対象から外す。git submodule 化はしない。

理由: 各プラットフォームのリポを完全に独立して扱える。submodule の運用(--recurse-submodules、コミットポインタ管理)の煩雑さを避けられる。kowai-operation 自体は運用ドキュメント・スクリプトのみを管理する方針とした。

DEVLOG(作業ログ)

開発日誌

このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。


2026-10-16

12:40 - iOS 5.2.2(FCM 版)の公開を確認しリリースタグを作成

やったこと:

  • iOS 5.2.2 が審査を通過して公開されたことを App Store Connect API で確認
    • 5.2.2 / appStoreState=READY_FOR_SALE / ビルド15、releaseType=MANUAL、段階的リリースなし(全利用者に一度に配信)
    • 審査への提出は 2026-10-15 17:36 JST(reviewSubmissions の submittedDate・state=COMPLETE)
  • kowai-ios に注釈付きのリリースタグ v5.2.2 を作成して push(打った先は efc0095)
    • ビルド15 のアップロードは 10-14 21:29 JST。efc0095「ビルド番号を 15 に」のコミットは 21:27:58 で、そのあとにコミットは無く作業ツリーもクリーン → 出荷ビルドのツリーと一致すると判断
    • 注釈には版・ビルド番号・最低 iOS 15.0・主な変更(Xcode 26 対応、FCM 化、https 化、YouTube のエラー153 の修正)・タグ位置の理由を書いた(Android v1.0.3 と同じ書き方)
    • 昔の iOS のタグ(v5.1.4_byXcode12.4 などの軽量タグ)の名前の付け方は踏襲せず、09-06 に採った v<versionName> 形式にそろえた
  • 10-13〜10-15 の FCM 移行作業は DEVLOG ではなく FCM_MIGRATION_PLAN.md(3-4・5-11・7-1a)に記録していたので、7-1a の表に申請と公開の行を足した

詰まったこと / 気づき:

  • app-store-connect MCP の最初の2回が fetch failed で失敗し、3回目で通った(シェルからの疎通は正常だった)。一時的な失敗なので、すぐ「使えない」と判断せず再試行する
  • Xcode のアーカイブ置き場(~/Library/Developer/Xcode/Archives)に 10-14 のアーカイブは無かった。ビルドとコミットの対応は、コミット時刻とアップロード時刻、HEAD 以降のコミットが無いことで判断した
  • 確認表(計画 8章)のうち「App Store 5.2.1 → 最終ビルドへの直接の上書き」と、緊急停止の本番での試行は、計画に結果の記録が無い(実施済みかは不明)

次回やること:

  • 計画 7-1 の 11: You の iPhone を App Store 版 5.2.2 に更新して ios_test が外れることを確認、初回の実配信(10-18 日曜夜の全配信)の後に kh_push_logs と実機での受信を確認

2026-09-06

14:55 - v1.0.3 の審査通過を確認しリリースタグを作成

やったこと:

  • Android 版 v1.0.3 が審査を通過して製品版で配信中であることを Play Developer API で確認 ([production] versionCodes=['11'] status=completed name=1.0.3)
  • kowai-android に**最初のリリースタグ v1.0.3(注釈付き)**を作成し push
    • 打った先は HEAD ではなく 4060c7d。出荷 AAB をビルドしたツリーがこのコミットだから
    • 4060c7d..HEAD の差分は app/build.gradle のみで、実質変更は no-op の debugSymbolLevel 1行だけ、残りは全てコメント(差分を取って確認済み)
    • 注釈に versionName / versionCode / targetSdk / AGP・Gradle バージョン / 主な変更 / タグ位置の理由を記載
  • kowai-android・kowai-operation・pocket-museum・subnote のいずれにもタグが1本も無かったため、v<versionName> 形式を今回の規約として採用

決めたこと:

  • ネイティブデバッグシンボルの vc11 への後付けは見送り、次回アップデート時に対応する(DECISIONS 参照)

詰まったこと / 気づき:

  • リリース後にコメント修正などのコミットを重ねていると、HEAD にタグを打つと「出荷物のソース」とズレる。タグは成果物をビルドしたツリーに打ち、ズレの有無を差分で確認してから決める
  • 次回アップデート時は、bundle と symbols を1つの edit にまとめれば API で添付できるので、Play Console の画面操作は不要になる。zip も Realm 10.19.0 のままなら .so が同一バイト列なので作り直し不要

2026-08-29

14:40 - 【訂正】ネイティブデバッグシンボルは後付け可能だった

やったこと:

  • .devnotes を expert-brains へ吸収する工程で、product-ops/knowledge/google-play-annual-compliance.md に 14:15 の結論への反証が既に記録されているのを発見
  • 反証内容を自分で再検証:
    • 作成済み zip 内の .so 4種と、配信中 AAB の base/lib/<abi>/librealm-jni.so の sha256 が全て一致
    • librealm-jni.so の .dynsym は 9191 エントリ(ナレッジ記載の実例と同数)
  • app/build.gradle のコメントと DECISIONS を訂正

訂正内容:

  • 誤: 「後付けアップロードは API・UI とも不可」→ 正: 後付けできないのは Play Developer API だけで、Play Console の「アプリバンドル エクスプローラ → 該当バージョン → ダウンロード タブ」からは手動で可能
  • 誤: 「.dynsym だけでは Realm 内部の関数が解決できず見返りが薄い」→ 正: .dynsym が残っていれば関数名まで解決できる。「strip 済み=シンボルなし」は早合点

詰まったこと / 気づき:

  • API が FAILED_PRECONDITION を返した時点で「UI も同じ制約だろう」と検証せずに断定してしまった。API の制約は UI の制約を意味しない
  • expert-brains に既に答えがあったのに、wrap-up の吸収工程まで参照しなかった。調査を始める前に該当 knowledge を引く習慣が要る

14:15 - 製品版昇格とネイティブデバッグシンボルの調査

やったこと:

  • 内部テストでの動作確認を経て、versionCode 11 を製品版へ昇格(トムソンの承認を得て You が Play Console で実施)。API で [production] versionCodes=['11'] status=completed name=1.0.3 を確認
  • Play Console の「ネイティブデバッグシンボルがアップロードされていません」警告を調査:
    • app/build.gradle 末尾でコメントアウトされていた debugSymbolLevel を有効化 → mergeReleaseNativeDebugMetadata が NO-SOURCE
    • librealm-jni.so の ELF セクションを実測 → .symtab なし / .debug_info なし / .dynsym のみ(220KB) = ストリップ済み
    • Realm の AAR 同梱物と Maven Central の 10.19.0 配下(aar / sources / javadoc のみ)を確認 → シンボル成果物は存在しない
    • 公開済み versionCode 11 への後付けアップロードを API で試行 → FAILED_PRECONDITION: The App Bundle with version code 11 is added previously - the Deobfuscation File can't be modified.
  • 調査結果を app/build.gradle のコメントとして記録し commit(f1d2977)

決めたこと:

  • ネイティブデバッグシンボルは Realm を使い続ける限り解消不能と結論。debugSymbolLevel は no-op のまま有効にして残す(DECISIONS 参照)

詰まったこと / 気づき:

  • AAB は「バンドルをアップロードするのと同じ edit の中」でしかシンボルを添付できない。APK 時代のような後付けは API・UI とも不可
  • AGP の debugSymbolLevel が対象にするのはプロジェクト自身がビルドしたネイティブライブラリのみで、AAR 依存に同梱された既製 .so は元々対象外。今回はそれに加えて .so 自体がストリップ済みという二重の理由で取れなかった

2026-08-28

19:40 - 内部テストが実機に配信されない問題の切り分け

やったこと:

  • Honor 20(YAL-L21H / Android 10 / SDK 29 / ファームウェア 2020-07 止まり)を USB 接続して adb で診断
  • Play ストアのアカウントは ichirokisanuki@gmail.com で正しく、Play プロテクト認定も「このデバイスは認定されています」を確認 → アカウント説・未認定説をいったん否定
  • 対照実験として Spotify の market://details?id=... を開いたところ同じ「利用できなくなりました」→ 端末側のディープリンク不調と判断
  • 実際の原因は You が発見: ブラウザが端末に入っていたもう一方の Google アカウントでログインしていたため、オプトインURL(Webページ)の承諾がそちらのアカウントに登録されていた。ブラウザからログアウトしたら即座に配信された
  • 配信後、実機で versionCode 11 / targetSdk 36 の起動を確認(例外ゼロ・広告表示 OK・レイアウト崩れなし)

詰まったこと / 気づき:

  • オプトインURLはWebページなので、Play ストアアプリで選択中のアカウントとブラウザのログインアカウントは別軸。 アプリ側だけ見て「アカウントは正しい」と誤って切り分けた。auto-memory に play-internal-test-optin-browser-account として記録
  • この端末は Android 10 止まりで Android 16 の検証には使えない。Huawei の GMS 事情から OS 更新も見込めない

14:30 - targetSdk 36 対応(Play の 2026/8/31 期限)と内部テスト公開

やったこと:

  • Play Console の「2026年8月31日までに対象 API レベルを更新してください」警告への対応
  • 失敗を切り分けられるよう段階を分けてビルド検証(各段階で BUILD SUCCESSFUL):
    1. ベースライン(AGP 8.6.1)
    2. Gradle 8.9 → 8.11.1 / AGP 8.6.1 → 8.9.1(compileSdk 35 のまま)← Realm の関門
    3. compileSdk / targetSdk 35 → 36
    4. bundleRelease(署名込み)
  • Realm 10.19.0 は AGP 8.9.1 で問題なく動作。 releaseRealmAccessorsTransformer も正常完走し、最大のリスクが空振りに終わった
  • AAB 内 librealm-jni.so の 4 ABI すべてが LOAD align=0x4000(16KB)を維持していることを実測確認
  • API 36 エミュレータ(fn_api36)で targetSdk 35 / 36 の A/B 検証 → 表示は完全に同一で回帰ゼロ
  • 積み残しを同梱:
    • MainActivity.kt の is_info を bool / int / string のどれで返っても壊れない形に変更
    • API 3本(stories / articles / movies)とプライバシーポリシー URL を http → https に統一。Kotlin コードから http:// が消滅
    • versionCode 11 / versionName 1.0.3
  • クリーンインストールで初回同期 4291 件・全4タブ・記事詳細を確認(例外ゼロ)
  • commit 4060c7d を push
  • Play Developer API 経由で内部テストトラックへアップロード・公開

決めたこと:

  • エッジツーエッジ修正は同梱せず分離(DECISIONS 参照)
  • ROADMAP にあった getInt(2) != 0 案は不採用(DECISIONS 参照)
  • Play へのアップロードは既存サービスアカウント + 独立スクリプト方式(DECISIONS 参照)

詰まったこと / 気づき:

  • ROADMAP に書いていた getBoolean(2) → getInt(2) != 0 は、サーバーが現在 bool を返している(S3 の全 4291 件を実測確認)ため org.json の getInt が JSONException を投げる。6月に直したクラッシュを逆向きに再発させるところだった
  • エッジツーエッジの強制は targetSdk 35 の時点で既に掛かっており、36 の変更は「オプトアウト属性の無効化」のみ。既存の表示崩れは 36 化とは無関係で、現行の製品版にも入っている
  • study-timer-ops 用に作った既存のサービスアカウント play-console-mcp@ichirokisanuki.iam.gserviceaccount.com が実録!怖い話へのリリース管理権限を既に持っていた。新規の準備は不要だった

次回やること:

  • エッジツーエッジ(insets)対応
  • Realm からの脱却

2026-06-09

18:00 - Android クラッシュ修正(HTTPS 301・is_html/is_info 型不一致 / S3 全件再 sync)

やったこと:

  • ユーザーレビュー報告で「Android で読めなくなった」「Enter ボタン押下でクラッシュ」「詳細ページに行くときクラッシュ」を受けて緊急修正
  • 原因 1(HTTPS 強制で旧クライアント詰まり)対応:
    • リリース済み iOS / Android アプリが http://kh.thomsonsapp.com/khApi/... を URL ハードコードしていたため、5/24 の 80 → 301 → HTTPS 化で詰まっていた
    • nginx の 80 番 server で /khApi/ だけ HTTP のままパススルー、認証系・管理画面は HTTPS 強制を維持(commit 0214fe2)
  • 原因 2(is_html / is_info 型不一致)対応:
    • Codex round1 medium-7 で「型統一」と判断して bool → str に変えた箇所が、Android Realm モデル StoryContent.kt の is_html: Boolean / is_info: Boolean と不整合
    • realm.createOrUpdateObjectFromJson が "0" を Boolean 変換できず JSONException → Realm transaction 内 uncaught → クラッシュ
    • schemas.py:StoryItem / stories.py:_to_item / cron.py:_story_struct の3箇所で str → bool に戻す(commit 0dcb65d)
    • all_story_contents_ids[i][2] も同様に bool(is_info) で出すよう修正(commit 2c345c9)
  • S3 既存ファイルの一括上書き:
    • cronStorySyncToS3 は is_s3_synced=0 のものだけ再 put する仕様のため、修正前に作られた 4000+ 個別 stories/{id:05d}.json が str のまま残っていた
    • 一回限りのスクリプト kowai-server/resync_all_stories.py を作って全 4189 件を bool 版で再 put(162 秒、失敗 0)
  • iOS への影響なし(SwiftyJSON の .boolValue は int / bool / "true"/"false" 文字列いずれも柔軟に解釈)
  • AVD kowai16k でエミュレーター起動、ユーザー側で実機 / エミュレーター検証 → OK 報告

詰まったこと / 気づき:

  • Codex round1 の「API と S3 で型を揃える」指摘を反映するときに、実際にこの JSON をパースしている Android Realm モデル(Boolean フィールド)の存在を確認しなかったのが致命的だった
  • 「Cake が string で返してた」は推測で、現役クライアントが何を要求するかは別問題
  • cronStorySyncToS3 の is_s3_synced=1 を信頼するクエリ設計は、JSON 仕様変更時に「全件再生成」できる経路を持たない弱点
  • 旧 80 番 HTTP は将来も残さざるを得ない(リリース済みアプリの baseUrl ハードコードを変えるには再リリースが必要)

次回やること:

  • 数日見守り(クラッシュレビューが収まるか)
  • 次の iOS リリース: kowai-ios/Kowai/Const.swift:14 の http://kh.thomsonsapp.com を https:// に
  • 次の Android リリース: HomeArticleViewModel.kt / HomeMovieViewModel.kt / StoryListViewModel.kt:33 の http:// を https:// に、MainActivity.kt:232 の getBoolean(2) を getInt(2) != 0 に
  • 全クライアントが HTTPS に揃ったら nginx の 80 番 /khApi/ パススルーを撤去して HTTPS only に戻す

2026-05-24

17:00 - Codex レビュー反映 → DNS 切替 → HTTPS 化(kowai-server 仕上げ)

やったこと:

  • Codex レビュー round1 反映(high 3 / medium 7 / low 数件):

    • high-1: 副作用エンドポイントを /khApi/* から分離。cron を /internal/*(nginx で 127.0.0.1 / ::1 のみ許可)、sendPushNotification / updateNotification* を /fearstoryPortal/api/*(require_login)。/khApi/* は読み出し API 4 本のみに整理
    • high-2: func.now() を datetime.now() に置換。RDS が UTC・アプリが JST 想定だったため、stories.py の公開判定で 9 時間ズレていた問題を解消
    • high-3: sendReservedNotification を UPDATE ... WHERE candidate=:val の atomic claim に変更。rowcount==1 の呼び出しだけが OneSignal 送信に進む
    • medium-1: YouTube publishedAt を aware UTC として epoch 取得、JST naive に変換して upload_date に保存
    • medium-2: RSS クローラの 1 サイト取り込みを 2 件に制限、PR/AD タイトル除外
    • medium-3: OneSignal 送信失敗を握って {"errors":[...], "status_code":N} の JSON で返す + 予約 push のログ message に OK/FAIL ステータスを埋める
    • medium-4: CSRF token 機構を追加(app/csrf.py の ensure_csrf_token / verify_csrf dependency、_base.html に <meta name="csrf-token"> と {% macro csrf() %} を定義し全 POST form に展開)
    • medium-5: /login?next= の open redirect を修正(/fearstoryPortal 配下の相対パスのみ許可)
    • medium-7: is_html / is_info の API vs S3 JSON 型不一致を str "0"/"1" に統一
    • low: cron に flock -n と curl --max-time を付与、DB URL を URL.create() でエスケープ
  • Codex round2 反映:

    • message 生成失敗時に [reserved/{tag}/{candidate}/FAIL:empty-message] をログ追加
    • /dbhealth を /internal/dbhealth に移して loopback 限定に
    • 本番では FastAPI の /docs /redoc /openapi.json を非公開化(KOWAI_ENABLE_DOCS=1 で開発時のみ有効)
    • deploy/README.md をフル更新(swap・cron 適用手順・ルーティング設計・安全運用メモを追加)
    • .env.example を全変数(DB / SESSION / ONESIGNAL / YOUTUBE / AWS)網羅
    • 旧 cron.txt に「廃止 / 参考用」のヘッダコメントを追加し誤適用を防ぐ
  • DNS 切替(kh.thomsonsapp.com → 54.249.174.108):

    • Route53 の親 thomsonsapp.com に kh A 54.249.174.108 を直接追加、親側の kh NS 委任レコードを削除(子 hosted zone への委任を停止)
    • dig で複数 resolver から 54.249.174.108 が返ることを確認
  • HTTPS 化:

    • Lightsail に snap で certbot をインストール、certbot certonly --webroot で Let's Encrypt 証明書取得(有効期限 2026-08-22)
    • Lightsail のファイアウォールで 443/TCP を Any IPv4 で許可
    • nginx を HTTPS server block に再構成: HSTS(max-age=1y; includeSubDomains)、TLSv1.2/1.3、HTTP→HTTPS 301 redirect、limit_req_zone で /login に rate limit(10req/min/IP)
    • SessionMiddleware に https_only=True を組み込み(KOWAI_INSECURE_COOKIES=1 で開発時無効化可)
  • 動作確認: https://kh.thomsonsapp.com/healthz 200 / HTTP→HTTPS 301 / HSTS 有効 / /khApi/stories 200 / /internal/* 外部から 403 / /fearstoryPortal 未認証 303→/login

詰まったこと / 気づき:

  • nginx 1.24 では http2 on; ディレクティブ未対応 → listen 443 ssl http2; の旧形式に直して解決
  • Lightsail はデフォルトで 443 が閉じている。インスタンスのファイアウォールで明示的に Any IPv4 で開けるまで HTTPS が通らない
  • RDS の time_zone が UTC のまま(Cake は datetime() を文字列で渡していたので JST 完結だったが、FastAPI で func.now() を使うと比較が UTC ベースになって 9 時間ズレる)。MySQL 側の @@time_zone を変えずに Python 側で datetime.now() に揃える方が変更量が小さい
  • CSRF dependency の await request.form() 先読みは、Starlette が form を cache するため後続の FastAPI Form(...) と協調する。ライブラリ実装に依存して動く前提のメモ
  • Lightsail に IAM ロールが直接アタッチできないため、S3 用に専用 IAM ユーザー kowai-server を新規発行(fearstory bucket に PutObject / GetObject / PutObjectAcl のみ)

次回やること:

  • 任意: 子 hosted zone kh.thomsonsapp.com を Route53 から削除(月 $0.50 節約)
  • 任意: 旧 Cake (fn-* EC2) の KhApi コントローラ・関連コード・kh.thomsonsapp.com vhost のクリーンアップ(fn-* EC2 自体は footballnext で継続使用)
  • 数日〜1週間の稼働観察(OneSignal 予約配信が予定時刻に届くか、S3 同期が継続するか)

2026-05-23

19:00〜26:00 - kowai-server (FastAPI) への移管・管理画面・cron 13本

やったこと:

  • 旧 Cake (footballnext-server) に間借りしていた /khApi/* と /fearstoryPortal/* を新 Lightsail インスタンス kowai-server に独立移管(FastAPI + Jinja2 + SQLAlchemy 2.0)
  • Lightsail Tokyo 1GB / 2 vCPU / 40GB を新規作成、VPC peering で既存 RDS (kowai schema) を共用、Static IP 54.249.174.108
  • 読み出し API 4 本(stories / movies / articles / spots)を Cake と完全一致になるよう移植(ID 系は str、bool 系は str "0"/"1"、digest_body は body[:70] → 改行除去 → HTML タグ除去)
  • fearstoryPortal 管理画面を Jinja2 SSR で全面移植: 広告比率(iOS/Android 各 5 テーブル一括)/ 作者・タグ(CRUD + ページング)/ 怖い話・心霊スポット・記事・RSS(一覧・編集・必要に応じて追加)/ 予約配信 send_time 編集・候補変更(SSR テーブル・現候補ハイライト・候補クリア)/ プッシュ通知(全配信・テスト送信)
  • 管理画面デザインを共通 CSS + Jinja2 base テンプレで刷新(モダンライト + 赤アクセント、カードレイアウト、テーブル hover、削除済み行ハイライト、sticky 保存バー、breadcrumb)
  • プッシュ通知: OneSignal credentials を旧 Cake KhOneSignal.php から移管、/khApi/sendPushNotification(全配信)と /khApi/sendPushNotificationTest(Subscription ID 指定)を実装、特定端末への送信を実機検証で OK
  • cron 13 本を FastAPI に移植: A 群 4 本(DB のみ)→ D 群 3 本(RSS / YouTube)→ B+C 群 6 本(S3 同期 / 集計データ)。S3 用に専用 IAM ユーザー(fearstory bucket への PutObject / GetObject / PutObjectAcl)を発行
  • crontab を新サーバーへ完全切り替え(Cake 側 cron は OFF、fn-* EC2 自体は footballnext で継続)
  • Lightsail 1GB プランで安定運用するためのチューニング: swap 1GB(fstab 永続化)、uvicorn --workers 2 --log-level warning、systemd LimitNOFILE=65536 / OOMScoreAdjust=-100、nginx gzip + keep-alive + /static cache

詰まったこと / 気づき:

  • Lightsail は EC2 IAM ロールが直接付けられないため、S3 認証用に専用 IAM ユーザーを発行する形になった
  • RDS の time_zone は UTC のまま(後の round1 review で公開判定 9 時間ズレに気付く)
  • 一部 RSS が Server disconnected without sending a response を返す。並行稼働中は Cake が拾っていたが、新サーバー完全切り替え後は user-agent 等の調整余地
  • 既存 KhOnesignalLogs テーブルに updated カラムが無い(created のみ)。models.py 生成時に気付かず修正
  • ssh 経由 curl で日本語を送ると URL encode の関係で文字化けする。ブラウザ経由なら問題なし
  • index.html のテーブルヘッダが sticky + table-wrap の overflow との組み合わせで崩れる → sticky を撤去
  • ボタンの色が出なかった: input[type=button].btn (specificity 21) が .btn-primary (specificity 10) より勝って background を白で上書きしていた CSS バグを修正

次回やること:

  • Codex に移管内容をレビュー依頼(.gods-talk)

18:34 - kowai-android 16KBページサイズ対応・新規リリース整備

やったこと:

  • 開発環境を新PCで再構築: Android Studio 2025.3(JBR 21内蔵)/ Android SDK(API 35・build-tools 35・platform-tools)/ コマンドラインツール
  • ビルドツールチェーン更新: Gradle 8.0→8.9 / AGP 8.1.0→8.6.1 / Kotlin 1.8.0→2.1.0、廃止された jcenter() を除去
  • jcenter廃止の巻き添えを解消: flexbox を新座標 com.google.android.flexbox へ、未使用の youtube-player-view(実体消失・コード未使用)を削除
  • compileSdk / targetSdk を 33→35(Play更新要件)、minSdk 21→24 に変更
  • nend SDK を完全削除(2024/3サービス終了・16KB対応版なし): 専用Kotlin5本+レイアウト5本を削除、広告ローテーション処理13ファイルから nend 分岐を除去
  • 広告SDKを16KB対応版へ: AdMob 22.3.0→25.2.0、ADG を +→2.36.0 にピン留め、Realm 10.15.1→10.19.0
  • ADG 2.36.0 のAPI変更(リスナー引数@NonNull化・getter群@Nullable化)に追従
  • リリース署名を keystore.properties 方式(gitignore対象)で build.gradle に組み込み
  • 署名済みリリースAABをビルドし16KBアラインメントを検証(ELF 2**14+zip整列、bundletool生成APKでも合格)
  • 既存バグ「リスト初回タップが無反応」を logcat で原因特定・修正、versionCode 10 を再ビルドし実機検証

詰まったこと / 気づき:

  • ネイティブ.soは Realm の librealm-jni.so のみ(AdMob・ADGは.so非同梱)。Realm 10.19.0 が16KB対応版で要件クリア
  • youtube-player-view 1.4.0 は jcenter専用で .aar がどこにも残っておらず、かつコード上完全に未使用 → 削除で解決
  • タップ無反応の原因は通信失敗ではなく BasicUtil.isDisableClickEvent() の3.5秒グローバル多重タップガード。logで10タップ中2遷移を確認し確定
  • このアプリは既に Play App Signing 登録済み(配信版が Google署名鍵)。kowai 鍵はアップロード鍵で、vc9アップロードが署名エラーなく通った事実が裏付け

次回やること:

  • vc10 のリリースAAB(kowai-android/app/build/outputs/bundle/release/app-release.aab)を Play Console にアップロードして公開(対応期限 2026-05-31)

18:27 - リポジトリ整備 & iOS通知失敗の原因調査・復旧

やったこと:

  • kowai-operation 配下にネストした独立リポ3つ(kowai-ios / kowai-android / footballnext-server)を .gitignore で分離
  • iOS の OneSignal プッシュ通知が4月以降届かない問題を調査
    • 送信経路を特定: footballnext-server の KhApiController.php::sendPushNotification() → KhOneSignal::send() → OneSignal REST API(included_segments で story / old_app に送信)
    • kh_onesignal_logs テーブルと OneSignal API(GET /notifications/{id})で配信実績を確認 → iOS 約1万台が全件 errored(APNs に拒否されている状態)
    • 原因を特定: OneSignal に登録された APNs 証明書の Bundle ID が jp.thomsons.ThomsonK002(誤・余計な K)。実アプリ(App Store / Xcode 両方で確認)は jp.thomsons.Thomson002。不一致のため APNs が全 push を拒否
  • 復旧テスト: 自分の端末1台に直接テスト送信 → ①更新前 errored:1 → 証明書更新 → ②更新後 successful:1(実機着信も確認)

詰まったこと / 気づき:

  • OneSignal 管理画面では証明書が「Active」「期限内(2026-12-28)」と表示され一見問題なく見えた。見るべきは expiry ではなく Bundle ID 行だった
  • sendPushNotification() は OneSignal が受理さえすれば「成功」扱いで、全件 errored でも検知できない。失敗発覚が遅れた一因
  • 当初「証明書の期限切れ」と推測したが誤り。実際は4月の証明書更新時に担当者が Bundle ID を打ち間違えたのが原因

次回やること:

  • 次の怖い話追加時に管理画面から本番送信し、約1万台への配信を確認
  • Apple Developer の誤 App ID jp.thomsons.ThomsonK002 を削除

最近のコミット

README

kowai-operation

概要

(記入予定)

セットアップ

(記入予定)

使い方

(記入予定)