← 一覧に戻る

schulte-operation

GitHub ↗ Python 最終push: 2026/10/13 18:34

WIP(現在進行中)

Work In Progress

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

現在の状況

Android 1.2.4 (vc9) を公開済み(ストア 1.2.4、タグ Android1.2.4)。Play の 16KB 警告の真因は共有ボタンのクラッシュ(NativeShare の FLAG_MUTABLE 欠落)で、修正済み。vitals の 2 週間後の再確認(2026-10-13)でも vc9 で再発なしで、この件は完了。iOS は 1.2.3 が公開中。

  • 2026-10-13: dev-timer #89 の vitals 再確認。vc9 は直近 60 日でクラッシュ・ANR とも issue 0 件。NativeShare(NativeShareBroadcastListener.Initialize)は 30 件・5 ユーザーで、出たのは vc6〜vc7 のみ。 vc9 の日次クラッシュ率は 9/28〜10/11 の 12 日分すべて 0.00%(1 日 60〜80 ユーザー、10/7〜8 は行なし)。#89 は done。 期限切れで残っていた #63(Android に iOS 1.2.3 の結果画面対応を入れる)も vc8 で同梱済み(iOS1.2.3 は Android1.2.4 の祖先)だったので done。このプロジェクトの dev-timer pending は 0 件
  • 2026-09-29: vitals で 1.2.4 (vc9) の共有クラッシュ解消を初回確認(dev-timer #85 は done)。この時点では vc9 の数日分しかなく弱い証拠だったので、上の 2 週間後の再確認につないだ
  • 2026-09-25: 16KB 警告(vc8)は 16KB 非対応ではなく、NativeShare.aar(2021-05)の PendingIntent に FLAG_MUTABLE が無く、共有ボタンで落ちる既存バグだった(vitals クラッシュ 1 位)。 問題のクラスだけ差し替えて vc9 を作り、You が実機確認して申請
  • 2026-09-11: スコア異常値フィルタと 24 時間ランキングの即時更新を本番投入(f4b0a67)。閾値未満の既存 10,182 行は del=1

配信状況

面 状態
iOS ストア 1.2.3(READY_FOR_SALE、タグ iOS1.2.3)
Android ストア 1.2.4 (vc9、タグ Android1.2.4 = schulte-unity bf0b855)。内部テストも vc9。vc8 は使わない(共有クラッシュあり)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. Play Console でアラート 3 件の表示が消えたか確認(前から残っている)
  2. journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る(前から残っている)
  3. (今四半期)schulte-unity を CodeCommit → GitHub に移行

Android のビルド・配信手順(今回確立)

  • 署名ビルドは Claude アプリの Bash からは通らない(~/Dropbox の keystore が macOS の権限で読めない)。アプリ内ターミナルタブで Unity -batchmode -projectPath <schulte-unity> -buildTarget Android -executeMethod CIBuild.BuildAndroid -ciSymbolsPublic -ciOutput <Builds/…aab> -logFile <Logs/…log>
  • 内部テストへ: uv run scripts/play_upload_internal.py <aab> <mapping.txt> <symbols.zip> "<リリース名>"。 製品版への昇格と審査送信は Play Console で行い、製品版に古い versionCode の下書きが残っていたら差し替えてから送る
  • schulte-unity の push は AWS_PROFILE=thomson-ik git push origin master
  • 16KB の確認: AAB の .so を ELF で検査(PT_LOAD の p_align・RELRO・ページ共有)+ エミュ kowai16k に bundletool build-apks --connected-device → install-apks。 エミュのデータ領域は一時なので、終了するとアプリは消える
  • vitals は Play Developer Reporting API(playdeveloperreporting v1beta1、SA ~/.config/gcloud/play-console-sa.json、scope https://www.googleapis.com/auth/playdeveloperreporting)で読める。uv run scripts/play_vitals.py [versionCode] で読める(読み取り専用。全バージョンの CRASH / ANR issue と、versionCode を渡すとその版の issue・日次クラッシュ率。--days N --pkg <pkg> で期間と他アプリも指定できる)。中身は google-auth の AuthorizedSession で REST を直接叩いているだけ
    • issue 単位: GET apps/<pkg>/errorIssues:search。filter=errorIssueType = CRASH AND versionCode = 9、orderBy=errorReportCount desc。 interval は interval.startTime.year / .month / .day と interval.startTime.timeZone.id=UTC のようにネストしたクエリで渡し、タイムゾーンは UTC のみ(America/Los_Angeles は 400「Unsupported timezone」、interval.startTime=<ISO 文字列> も 400、interval を省略すると空 {} が返る)
    • クラッシュ率: POST apps/<pkg>/crashRateMetricSet:query(dimensions versionCode、metrics crashRate / userPerceivedCrashRate / distinctUsers)。件数: POST apps/<pkg>/errorCountMetricSet:query(reportType が必須 dimension。ここに crashRate を渡すと 400)。 どちらも DAILY の timelineSpec は timeZone.id=America/Los_Angeles、日付は year/month/day だけ
    • 最新日は GET apps/<pkg>/crashRateMetricSet(errorCountMetricSet)の freshnessInfo。9/29 20 時の時点で、crashRate の DAILY は 9/27 分まで、エラー側の HOURLY は 17 時ごろまで
    • 少人数のアプリなので distinctUsers は 10 単位の値で返り、9/25〜26 のようにバージョン別の行が出ない日がある。件数も小さいので「0 件」は弱い証拠になりやすい

本番 DB の集計・確認のやり方

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • 勉強タイマー・kowai-best・断食タイマーにも同じ未修正の NativeShare(それぞれ vitals のクラッシュ 1 位)。各プロジェクトの .devnotes に記録済み。修正は各アプリの次回アップデートのときに直す(2026-09-29、You 判断)
  • NativeShare をプラグインごと上流 v1.5.5 に上げるか(今回は 1 クラスだけのパッチ)
  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

過去のWIPアーカイブ

2026-10-13 18:33 時点のスナップショット(更新前)

Android 1.2.4 (vc9) を公開済み(ストア 1.2.4、タグ Android1.2.4)。Play の 16KB 警告の真因は共有ボタンのクラッシュ(NativeShare の FLAG_MUTABLE 欠落)で、修正済み(vitals でも 1.2.4 で再発なしを 2026-09-29 に確認)。iOS は 1.2.3 が公開中。

  • 2026-09-29: vitals で 1.2.4 (vc9) の共有クラッシュ解消を確認(dev-timer #85 は done)。NativeShareBroadcastListener.Initialize は直近 60 日で 35 件・5 ユーザー、出たのは vc6〜vc7 のみ。vc9 は 9/20 以降クラッシュ・ANR とも issue 0 件、クラッシュ率は 9/27 分が 0.00%(約 70 ユーザー)。 旧版でも 60 日で 5 ユーザーしか踏まないバグなので、vc9 の数日分は強い証拠ではない(反する材料はなく、実機確認は済み)。2 週間後に再確認する(dev-timer #89、10/13)
  • 2026-09-25: 16KB 警告(vc8)は 16KB 非対応ではなく、NativeShare.aar(2021-05)の PendingIntent に FLAG_MUTABLE が無く、共有ボタンで落ちる既存バグだった(vitals クラッシュ 1 位)。 問題のクラスだけ差し替えて vc9 を作り、You が実機確認して申請
  • 2026-09-11: スコア異常値フィルタと 24 時間ランキングの即時更新を本番投入(f4b0a67)。閾値未満の既存 10,182 行は del=1

配信状況

面 状態
iOS ストア 1.2.3(READY_FOR_SALE、タグ iOS1.2.3)
Android ストア 1.2.4 (vc9、タグ Android1.2.4 = schulte-unity bf0b855)。内部テストも vc9。vc8 は使わない(共有クラッシュあり)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. 2 週間後(10/13 20:21 に通知、dev-timer #89)に vitals で NativeShare のクラッシュ再発なしを再確認する。新版の利用者が増えたあとの確認(uv run scripts/play_vitals.py 9)
  2. Play Console でアラート 3 件の表示が消えたか確認(前から残っている)
  3. journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る(前から残っている)

Android のビルド・配信手順(今回確立)

  • 署名ビルドは Claude アプリの Bash からは通らない(~/Dropbox の keystore が macOS の権限で読めない)。アプリ内ターミナルタブで Unity -batchmode -projectPath <schulte-unity> -buildTarget Android -executeMethod CIBuild.BuildAndroid -ciSymbolsPublic -ciOutput <Builds/…aab> -logFile <Logs/…log>
  • 内部テストへ: uv run scripts/play_upload_internal.py <aab> <mapping.txt> <symbols.zip> "<リリース名>"。 製品版への昇格と審査送信は Play Console で行い、製品版に古い versionCode の下書きが残っていたら差し替えてから送る
  • schulte-unity の push は AWS_PROFILE=thomson-ik git push origin master
  • 16KB の確認: AAB の .so を ELF で検査(PT_LOAD の p_align・RELRO・ページ共有)+ エミュ kowai16k に bundletool build-apks --connected-device → install-apks。 エミュのデータ領域は一時なので、終了するとアプリは消える
  • vitals は Play Developer Reporting API(playdeveloperreporting v1beta1、SA ~/.config/gcloud/play-console-sa.json、scope https://www.googleapis.com/auth/playdeveloperreporting)で読める。uv run scripts/play_vitals.py [versionCode] で読める(読み取り専用。全バージョンの CRASH / ANR issue と、versionCode を渡すとその版の issue・日次クラッシュ率。--days N --pkg <pkg> で期間と他アプリも指定できる)。中身は google-auth の AuthorizedSession で REST を直接叩いているだけ
    • issue 単位: GET apps/<pkg>/errorIssues:search。filter=errorIssueType = CRASH AND versionCode = 9、orderBy=errorReportCount desc。 interval は interval.startTime.year / .month / .day と interval.startTime.timeZone.id=UTC のようにネストしたクエリで渡し、タイムゾーンは UTC のみ(America/Los_Angeles は 400「Unsupported timezone」、interval.startTime=<ISO 文字列> も 400、interval を省略すると空 {} が返る)
    • クラッシュ率: POST apps/<pkg>/crashRateMetricSet:query(dimensions versionCode、metrics crashRate / userPerceivedCrashRate / distinctUsers)。件数: POST apps/<pkg>/errorCountMetricSet:query(reportType が必須 dimension。ここに crashRate を渡すと 400)。 どちらも DAILY の timelineSpec は timeZone.id=America/Los_Angeles、日付は year/month/day だけ
    • 最新日は GET apps/<pkg>/crashRateMetricSet(errorCountMetricSet)の freshnessInfo。9/29 20 時の時点で、crashRate の DAILY は 9/27 分まで、エラー側の HOURLY は 17 時ごろまで
    • 少人数のアプリなので distinctUsers は 10 単位の値で返り、9/25〜26 のようにバージョン別の行が出ない日がある。件数も小さいので「0 件」は弱い証拠になりやすい

本番 DB の集計・確認のやり方

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • 勉強タイマー・kowai-best・断食タイマーにも同じ未修正の NativeShare(それぞれ vitals のクラッシュ 1 位)。各プロジェクトの .devnotes に記録済み。修正は各アプリの次回アップデートのときに直す(2026-09-29、You 判断)
  • NativeShare をプラグインごと上流 v1.5.5 に上げるか(今回は 1 クラスだけのパッチ)
  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-29 20:14 時点のスナップショット(更新前)

Android 1.2.4 (vc9) を公開済み(ストア 1.2.4、タグ Android1.2.4)。Play の 16KB 警告の真因は共有ボタンのクラッシュ(NativeShare の FLAG_MUTABLE 欠落)で、修正済み。iOS は 1.2.3 が公開中。

  • 2026-09-25: 16KB 警告(vc8)は 16KB 非対応ではなく、NativeShare.aar(2021-05)の PendingIntent に FLAG_MUTABLE が無く、共有ボタンで落ちる既存バグだった(vitals クラッシュ 1 位)。 問題のクラスだけ差し替えて vc9 を作り、You が実機確認して申請
  • 2026-09-11: スコア異常値フィルタと 24 時間ランキングの即時更新を本番投入(f4b0a67)。閾値未満の既存 10,182 行は del=1

配信状況

面 状態
iOS ストア 1.2.3(READY_FOR_SALE、タグ iOS1.2.3)
Android ストア 1.2.4 (vc9、タグ Android1.2.4 = schulte-unity bf0b855)。内部テストも vc9。vc8 は使わない(共有クラッシュあり)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. 公開 3〜4 日後に、vitals で NativeShareBroadcastListener.Initialize のクラッシュが 1.2.4 で出ていないか確認(dev-timer #85、9/29 20:06 に通知)
  2. Play Console でアラート 3 件の表示が消えたか確認(前から残っている)
  3. journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る(前から残っている)

Android のビルド・配信手順(今回確立)

  • 署名ビルドは Claude アプリの Bash からは通らない(~/Dropbox の keystore が macOS の権限で読めない)。アプリ内ターミナルタブで Unity -batchmode -projectPath <schulte-unity> -buildTarget Android -executeMethod CIBuild.BuildAndroid -ciSymbolsPublic -ciOutput <Builds/…aab> -logFile <Logs/…log>
  • 内部テストへ: uv run scripts/play_upload_internal.py <aab> <mapping.txt> <symbols.zip> "<リリース名>"。 製品版への昇格と審査送信は Play Console で行い、製品版に古い versionCode の下書きが残っていたら差し替えてから送る
  • schulte-unity の push は AWS_PROFILE=thomson-ik git push origin master
  • 16KB の確認: AAB の .so を ELF で検査(PT_LOAD の p_align・RELRO・ページ共有)+ エミュ kowai16k に bundletool build-apks --connected-device → install-apks。 エミュのデータ領域は一時なので、終了するとアプリは消える
  • vitals は Play Developer Reporting API(playdeveloperreporting v1beta1、SA ~/.config/gcloud/play-console-sa.json)の vitals.errors.issues.search で読める

本番 DB の集計・確認のやり方

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • 勉強タイマー・kowai-best・断食タイマーにも同じ未修正の NativeShare(それぞれ vitals のクラッシュ 1 位)。各プロジェクトの .devnotes に記録済みで、修正は後日
  • NativeShare をプラグインごと上流 v1.5.5 に上げるか(今回は 1 クラスだけのパッチ)
  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-25 20:07 時点のスナップショット(更新前)

Android 1.2.4 (vc9) を公開済み(ストア 1.2.4、タグ Android1.2.4)。Play の 16KB 警告の真因は共有ボタンのクラッシュ(NativeShare の FLAG_MUTABLE 欠落)で、修正済み。iOS は 1.2.3 が公開中。

  • 2026-09-25: 16KB 警告(vc8)は 16KB 非対応ではなく、NativeShare.aar(2021-05)の PendingIntent に FLAG_MUTABLE が無く、共有ボタンで落ちる既存バグだった(vitals クラッシュ 1 位)。 問題のクラスだけ差し替えて vc9 を作り、You が実機確認して申請
  • 2026-09-11: スコア異常値フィルタと 24 時間ランキングの即時更新を本番投入(f4b0a67)。閾値未満の既存 10,182 行は del=1

配信状況

面 状態
iOS ストア 1.2.3(READY_FOR_SALE、タグ iOS1.2.3)
Android ストア 1.2.4 (vc9、タグ Android1.2.4 = schulte-unity bf0b855)。内部テストも vc9。vc8 は使わない(共有クラッシュあり)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. 公開 3〜4 日後に、vitals で NativeShareBroadcastListener.Initialize のクラッシュが 1.2.4 で出ていないか確認(dev-timer で通知)
  2. Play Console でアラート 3 件の表示が消えたか確認(前から残っている)
  3. journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る(前から残っている)

Android のビルド・配信手順(今回確立)

  • 署名ビルドは Claude アプリの Bash からは通らない(~/Dropbox の keystore が macOS の権限で読めない)。アプリ内ターミナルタブで Unity -batchmode -projectPath <schulte-unity> -buildTarget Android -executeMethod CIBuild.BuildAndroid -ciSymbolsPublic -ciOutput <Builds/…aab> -logFile <Logs/…log>
  • 内部テストへ: uv run scripts/play_upload_internal.py <aab> <mapping.txt> <symbols.zip> "<リリース名>"。 製品版への昇格と審査送信は Play Console で行い、製品版に古い versionCode の下書きが残っていたら差し替えてから送る
  • schulte-unity の push は AWS_PROFILE=thomson-ik git push origin master
  • 16KB の確認: AAB の .so を ELF で検査(PT_LOAD の p_align・RELRO・ページ共有)+ エミュ kowai16k に bundletool build-apks --connected-device → install-apks。 エミュのデータ領域は一時なので、終了するとアプリは消える
  • vitals は Play Developer Reporting API(playdeveloperreporting v1beta1、SA ~/.config/gcloud/play-console-sa.json)の vitals.errors.issues.search で読める

本番 DB の集計・確認のやり方

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • 勉強タイマー・kowai-best・断食タイマーにも同じ未修正の NativeShare(それぞれ vitals のクラッシュ 1 位)。各プロジェクトの .devnotes に記録済みで、修正は後日
  • NativeShare をプラグインごと上流 v1.5.5 に上げるか(今回は 1 クラスだけのパッチ)
  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-25 19:31 時点のスナップショット(更新前)

スコアの異常値フィルタと 24 時間ランキングの即時更新を本番投入済み(schulte-server-v2 f4b0a67)。閾値未満の既存 10,182 行は del=1 済み。iOS 1.2.3 (1) は TestFlight 配布中、schulte-unity は未コミット。

  • 2026-09-11: sendScore でモード別下限未満(4x4 2 秒 / 5x5 4 秒 / 6x6 7 秒 / 7x7 15 秒)を保存せず成功応答(8c7ffac)。決め方もトムソンに共有済み
  • 2026-09-11: トムソンの指示で閾値未満の既存行に del=1(計 10,182 行、対象 id は Lightsail ~/del-bogus-20260911/)。S3 全 20 ファイル再生成済み。 閾値直上に同じ偽造 ID の古い行が残り全期間トップになっているが、閾値内なので BAN しない(You 判断)
  • 2026-09-11: 24 時間ベスト更新時に当該モードの daily(TSV/JSON)を即時再生成(f4b0a67)。cron の daily は 30 分ごとの保険に変更
  • 2026-09-09: 結果画面を「ランキング常時表示、自分の行を赤/薄い赤で区別」に変更し TestFlight にアップロード
  • 2026-09-08: ランキングを「1 人 1 行(自己ベストのみ)」に変更してデプロイ(05ec7b8)

配信状況

面 状態
iOS ストア 1.2.2 / TestFlight 1.2.3 (1) VALID、内部グループ「aaa」に自動配布
Android ストア 1.2.3 (vc7)。結果画面の変更は未同梱(次は 1.2.4 / vc8)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. TestFlight で 1.2.3 の結果画面を確認 → schulte-unity を CodeCommit にコミットしタグ iOS1.2.3 → App Store 審査提出
  2. Android 1.2.4 (vc8) を同じコードでビルド(Unity の Create symbols.zip を Public にしてから)→ 内部テスト → 製品版
  3. Play Console でアラート 3 件の表示が消えたか確認
  4. 数日後に journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る

本番 DB の集計・確認のやり方(今回整備)

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み(閾値直上に偽造 ID の古い行が残るのは承知のうえ)。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)。 段階案・実測値・移行策の詳細は DEVLOG 2026-08-30 と過去の WIP アーカイブ参照
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-11 19:14 時点のスナップショット(更新前)

スコアの異常値フィルタと 24 時間ランキングの即時更新を本番投入済み(schulte-server-v2 f4b0a67)。iOS 1.2.3 (1) は TestFlight 配布中、schulte-unity は未コミット。

  • 2026-09-11: sendScore でモード別下限未満(4x4 2 秒 / 5x5 4 秒 / 6x6 7 秒 / 7x7 15 秒)を保存せず成功応答(8c7ffac)。トムソンに共有済み
  • 2026-09-11: 24 時間ベスト更新時に当該モードの daily(TSV/JSON)を即時再生成(f4b0a67)。cron の daily は 30 分ごとの保険に変更
  • 2026-09-09: 結果画面を「ランキング常時表示、自分の行を赤/薄い赤で区別」に変更し TestFlight にアップロード
  • 2026-09-08: ランキングを「1 人 1 行(自己ベストのみ)」に変更してデプロイ(05ec7b8)

配信状況

面 状態
iOS ストア 1.2.2 / TestFlight 1.2.3 (1) VALID、内部グループ「aaa」に自動配布
Android ストア 1.2.3 (vc7)。結果画面の変更は未同梱(次は 1.2.4 / vc8)
サーバー schulte-server-v2 f4b0a67(Lightsail 52.68.176.128、ichirokisanuki 側 AWS)

次にやること

  1. 閾値未満の既存行(約 1 万行)を del=1 にするか決める(You 判断。やればランキングから即消える、アプリ更新不要)
  2. TestFlight で 1.2.3 の結果画面を確認 → schulte-unity を CodeCommit にコミットしタグ iOS1.2.3 → App Store 審査提出
  3. Android 1.2.4 (vc8) を同じコードでビルド(Unity の Create symbols.zip を Public にしてから)→ 内部テスト → 製品版
  4. Play Console でアラート 3 件の表示が消えたか確認
  5. 数日後に journal で daily ranking refreshed / refresh failed / rejected bogus score の件数を眺め、想定(ベスト更新 600 件/日前後)と合っているか見る

本番 DB の集計・確認のやり方(今回整備)

ssh -i ~/.ssh/lightsail-default-ap-northeast-1.pem -o ConnectionAttempts=3 ubuntu@52.68.176.128 → cd ~/apps/schulte-server-v2 && PYTHONPATH=. ~/.local/bin/uv run python /tmp/script.py(app.db.SessionLocal で本番 RDS に繋がる)。 重い並べ替えは MySQL のウィンドウ関数側で行い、長いものは nohup … > /tmp/out & で流す(1GB RAM で Python に全行載せると OOM)。 テスト送信は unique_key を v2-test- プレフィックスにして後で削除する。

未決事項

  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策: 下限フィルタは対症療法として投入済み。本命はセッショントークン +「証明付きのスコアだけランキング掲載」(次回アプデ以降)。 段階案・実測値・移行策の詳細は DEVLOG 2026-08-30 と過去の WIP アーカイブ参照
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-11 18:42 時点のスナップショット(更新前)

iOS 1.2.3 (1) が TestFlight で配布可能になった(処理完了)。schulte-unity の変更は未コミット(コミットとタグ iOS1.2.3 は You の判断待ち)。

  • 2026-09-08: ランキングをサーバー側で「1 人 1 行(自己ベストのみ)」に変更してデプロイ済み(schulte-server-v2 05ec7b8、RDS に idx_del_player_score 追加)
  • 2026-09-09: 結果画面を「ランキング常時表示、自分の行を赤/薄い赤で区別」に変更。実機(iPhone 14)で確認し、TestFlight にアップロード
  • 2026-09-09: 24 時間ランキングの即時更新(イベント駆動)を試算。負荷は問題なし、未実装

配信状況

面 状態
iOS ストア 1.2.2 / TestFlight 1.2.3 (1) VALID、内部グループ「aaa」に自動配布
Android ストア 1.2.3 (vc7)。結果画面の変更は未同梱(次は 1.2.4 / vc8)
サーバー schulte-server-v2 05ec7b8(Lightsail、Thomson 側 AWS ではなく ichirokisanuki 側)

次にやること

  1. TestFlight で 1.2.3 の結果画面を確認 → schulte-unity を CodeCommit にコミットしタグ iOS1.2.3 → App Store 審査提出
  2. Android 1.2.4 (vc8) を同じコードでビルド(Unity の Create symbols.zip を Public にしてから)→ 内部テスト → 製品版
  3. Play Console でアラート 3 件の表示が消えたか確認
  4. (検討)24 時間ランキングのイベント駆動即時更新。sendScore 後にバックグラウンドで daily を再生成、cron は 30 分の保険に

実機確認の手順(iOS、今回整備)

Unity -batchmode -buildTarget iOS -executeMethod CIBuild.BuildIOS → xcodebuild(DEVELOPMENT_TEAM=JFBRCYHN24、自動署名)→ xcrun devicectl device install app。1 サイクル約 5 分。TestFlight は xcodebuild archive → -exportArchive(destination upload)。 詳細は DEVLOG 2026-09-09 参照

未決事項

  • インタースティシャル広告が閉じられない事象(AppLovin 経由の TikTok プレイアブルが 3 分以上ループ)。再現性は未確認
  • ランキングのスコア偽造対策(次回以降): 本命はセッショントークン +「証明付きのスコアだけランキング掲載」。 「自己ベスト中心」は 2026-09-08 に実施済みで、偽造者が何百行も占有する見え方は消えた。 段階案・実測値・移行策の詳細は DEVLOG 2026-08-30 と過去の WIP アーカイブ参照
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している
  • thomson-share スキルはこのプロジェクトには未配置(visionworkout-operation / study-timer-ops にある)

2026-09-11 16:57 時点のスナップショット(更新前 / TestFlight アップロード直後)

Android 16 / Billing 8 対応版 (vc7 / 1.2.3) がリリース済み。8/31 期限への対応は完了。

その後、ロケール由来のスコア送信失敗をサーバー側で修正し、証明書関連の懸念も解消した。 2026-09-08 にランキングを「プレイヤーごとの自己ベストのみ」に変更してデプロイ済み(サーバーのみ、アプリ更新不要)。 2026-09-09 に結果画面を「ランキング常時表示、自分の行を赤/薄い赤で区別」に変更し、実機で確認。 iOS 1.2.3 (1) を TestFlight にアップロード済み(処理待ち)。schulte-unity は未コミット。

Play Console の状態

production   status=completed   vc=7 (1.2.3)   ← リリース済み
internal     status=completed   vc=7 (1.2.3)
  • targetSdk 36 / Billing Library 8.3.0 / appCategory="game" 付与済み
  • アラート①③は提出時点で解消。②「新規ユーザーに配信されない」は審査通過をもって解消

検証済み

  • スコア送信: 実機で 200 OK を確認(旧版 1.2.2 / 新版 1.2.3 の両方)。ランキング反映も確認
  • Android 16: edge-to-edge の見切れなし。大画面(sw720dp 再現)でも縦固定が維持される(appCategory=game が機能)
  • サーバー: 証明書は 2026-11-26 まで有効、certbot renew --dry-run 成功、認証方式は webroot

判明したこと

Android には課金導線が存在しない。 #if UNITY_IOS / #if UNITY_ANDROID で無効化されており、 Play Console に商品が無ければそもそも初期化が失敗する。一度も Android で購入できた状態にはなっていない (コード上は 2025-03〜2026-01 の間だけ初期化を試みる状態だった)。 Billing 8 更新で壊れる余地は無い。

完了済み(vc7 リリース関連):

  • Android版 1.2.3 (vc7) が審査通過しリリース済み。タグ Android1.2.3 を 6b09c37c1 に付与
  • ネイティブデバッグシンボルを Play Console の画面からアップロード済み
    • ファイル: schulte-unity/Builds/schulte-1.2.3-vc7-native-debug-symbols.zip(21MB、build-id 照合済み)
    • API では後付け不可(バンドルと同じ編集セッション内でしか添付できない)ため画面から実施
  • SNS トピック SMS_Alert の購読に ichirokisanuki@gmail.com を追加(承認済み)
  • カンマ小数点ロケールでスコア送信が失敗していたバグをサーバー側で修正(3ff3a20、デプロイ済み)
    • 24時間で約2.8%の送信が 422 で失敗していた。既存の全バージョンが即座に救われる
    • これで監視の通知先は3件すべて承認済み。次の証明書警告は 2026-10-27 ごろ(期限30日前)
  • ランキングをプレイヤーごとの自己ベストのみに変更(schulte-server-v2 05ec7b8、2026-09-08 デプロイ済み)
    • _fetch_ranking_rows() を GROUP BY player_id + MIN(score) に書き換え、is_ban = 1 も除外
    • 本番 RDS の modeN_scores 4 テーブルに idx_del_player_score (del, player_id, score) を追加済み
    • S3 の全 12 ファイルで「行数 = ユニーク人数」を確認。全期間の生成は最大 1.4 秒(30 分ごと)

次にやること:

  1. Play Console でアラート3件の表示が実際に消えたか確認(反映に時間差あり)
  2. Unity の Publishing Settings → Create symbols.zip を Public にする(次回から自動生成)
  3. iOS 1.2.3 リリース作業(TestFlight アップロード済み)
    • 内容: 結果画面をランキング常時表示 + 自分の行を赤/薄い赤で区別(RankingRow.Setup(isMeDimmed) / ResultWithRankingAdditiveScene.SetupRankingArea / LocalDB.MergeScoreThisTime)、 SendScore の InvariantCulture、CIBuild.BuildIOS 追加、iOS ビルド番号 1
    • 残作業: TestFlight で確認 → schulte-unity を CodeCommit にコミットしタグ iOS1.2.3 → App Store 審査提出
    • Android は次のバージョン(1.2.4 / vc8)で同じコードを同梱する(symbols.zip を Public にしてからビルド)

未決事項:

  • インタースティシャル広告が閉じられない事象。AppLovin 経由の TikTok プレイアブルが3分以上ループし、 閉じるボタンも戻るキーも効かず、結果画面に到達できずスコアが失われた。再現性は未確認
  • ランキングのスコア偽造対策(次回アプデで検討)
    • 422 とは無関係であることを 2026-08-30 に確認済み(422 は正規アプリからのロケール由来バグ)
    • 根本原因: サーバーは「プレイされた事実」を一切持っていない。 ゲーム開始を知らせるエンドポイントが無く、unique_key もクライアント生成 (DateTime.Now + ランダム7文字)で偽造可能。二重送信防止の役割しかない
    • 実測されている偽造の例(各モードの最小値とタップ速度): 4x4=0.35秒(46回/秒) / 5x5=0.92秒(27回/秒) / 6x6=1.99秒(18回/秒) / 7x7=5.50秒(8.9回/秒)。 中央値は 7.08 / 15.4 / 30.5 / 60.7 秒。6x6 は 2.70 と 4.59 の間に明確なギャップあり
    • 対策の段階:
      1. 下限チェック + is_ban 活用 + 既存データ掃除(サーバーのみ) → 対症療法。閾値のすぐ上に逃げられ、しかも人間に可能な範囲なので検出不能になる
      2. セッショントークン(開始時に発行→送信時に照合)。サーバーが実経過時間を知るので 申告値 < 実経過時間 を嘘と判定できる → 物理的にありえない時間が本当に不可能になる
      3. 盤面をサーバー生成 + タップ列の検証 → 「待ってから申告」も困難に
      4. Play Integrity / App Attest → 「本物のアプリからか」を証明
    • 移行時の課題: 証明の無いリクエストを拒否すると旧バージョンが全滅する。 回避策は「証明付きのスコアだけランキングに載せる」。旧クライアントは送信も自己ベストも 従来どおりで、ランキング掲載のみ対象外。移行期間を切る必要がない
    • 戦略的な代替案: ランキングの位置づけ自体を下げる(自己ベスト中心 / 期間を短く / フレンド内のみ)。実装コストほぼゼロで偽造の旨味も消える → 「自己ベスト中心」は 2026-09-08 に実施済み。偽造者が何百行も占有する見え方は消えた
    • 現時点の所感: 段階2 +「証明付きのみ掲載」が費用対効果の山。段階3以上は過剰の可能性
    • is_ban は 2026-09-08 から _fetch_ranking_rows() で参照するようになった(BAN 済み 4 人はランキングに出ない)。 ただし手動発見が前提な点は変わらない
  • Android に課金を新規実装するかは未判断(「復活」ではなく新規実装)
  • 7channel の証明書が同じ壊れ方をしないか → 2026-08-30 確認済み・問題なし。 7channel は最初から webroot(HTTP-01)方式で AWS 認証情報に依存しておらず、 2026-08-03 に自動更新が成功済み(期限 11-01)。certbot renew --dry-run も成功。 加えて監視の cert:443 が残30日で警告するため二重に担保されている
  • エミュレータ fn_api36 に schulte をサイドロードしたまま残している

2026-08-29 13:43 時点のスナップショット(更新前 / 内部テスト配信直後)

Android 16 / Billing 8 対応版 (versionCode 7 / 1.2.3) を内部テストに配信済み。並行して SSL 失効による33日間の障害を復旧。

Android(Google Play の 8/31 期限)

  • internal トラック = versionCode 7 (1.2.3)、production = 6 (1.2.2) のまま
  • targetSdk 36 / Billing Library 8.3.0 / appCategory="game" 付与済み
  • Unity は 2022.3.62f2 のまま(エディタ更新は不要だった)
  • com.unity.purchasing は 4.15.1(5.x ではない)。旧 API のままなので PurchasingManager.cs は無変更

サーバー(復旧済み)

  • Let's Encrypt 証明書が 2026-07-26 に失効し、スコア送信が33日間停止していた
  • certbot の認証方式を dns-route53 → webroot(HTTP-01) に変更して復旧
  • 現在の証明書は 2026-11-26 まで有効。certbot renew --dry-run も成功
  • 監視 (senrigan-observer) に証明書残日数チェック(残30日で警告)と NG 継続時の24時間ごと再通知を追加

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

  1. 実機で内部テスト版を確認 — 定期購読 jp.thomsons.schulte.membership.1year / 買い切り jp.thomsons.schulte.noads の購入と復元、Android 16 での UI 見切れ、タブレットでの縦固定維持
  2. 8/31 までに判断 — 製品版への昇格、または Play Console から 11/1 への期限延長申請
    • アラート②「新規ユーザーに配信されない」は製品版トラックが API 35 以上でないと解消しない。内部テストだけでは消えない
  3. SNS の購読に普段使いの ichirokisanuki@gmail.com を追加(AWS コンソールでのメール確認が必要)
  4. schulte-unity を CodeCommit → GitHub に移行(今四半期のロードマップ項目、未着手)

未決事項:

  • 7channel の証明書は 2026-11-01 期限(残64日)。同じ certbot 構成なら同じ壊れ方をする可能性があるが未確認
  • multipurpose-lightsail は IP 直指定のため証明書チェックを付けていない
  • 認証機構の追加(チート対策)は v1 から引き続き未実装

2026-08-28 18:50 時点のスナップショット(更新前 / 2026-04-28 の内容)

🎉 schulte v1 → v2 移行 + 撤退 + RDS Private 化、全完了。月額 約 $24 削減 + セキュリティ向上。

  • v2 (Lightsail / FastAPI / Python 3.13) が schulte.thomsonsapp.com の 100% トラフィック処理
  • swap 1GB + uvicorn workers=2 で長期稼働の安定性強化済み
  • 旧 v1 EC2 (1号機・2号機) は完全削除(terminate)
  • ALB taki から schulte 関連 (リスナールール × 2 / TG) を完全除去(taki ALB 自体は他4アプリ用に残置)
  • Lambda 3つ (TCM_observeALB_*) から Schulte 行コメントアウト済
  • Route53 から alb-v1 レコード削除済、lightsail-v2 のみ
  • RDS footballnext baseballnext を PubliclyAccessible=false 化(月 -$7.3、セキュリティ向上)
  • EIP の状況: cron 用 1個のみ存続、他は全部 release or AWS 自動解放
  • dev-timer #3 (4/28 7:42) はもう過ぎてしまった(観測は wrap-up 直前で実施、問題なし)

月額コスト効果(最終):

項目 削減
EC2 t3.nano × 2 -$15.2
EBS 8GB × 2 -$1.6
RDS 用 EIP × 2 -$7.3
合計削減 -$24.1
Lightsail micro_3_0 (新規) +$7.0
正味 約 -$17.1/月(年 約$205)

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

  1. (別プロジェクトで)共通 phpmyadmin (~/cdev/common-php-my-admin) を SSH トンネル経由に改修
    • 引き継ぎ.md を作成済み、別 Claude セッションが即作業開始できる状態
  2. (オプション)kizuna RDS も PubliclyAccessible=false 化(月 -$3.65 追加削減、phpmyadmin SSH トンネル整備後)
  3. schulte-unity を CodeCommit → GitHub に移行(今四半期内のロードマップ項目、本当の残課題)
  4. 「いつか」項目(認証機構 / レート制限 / メトリクス / pytest / Docker compose / CloudWatch Logs 集約)

未決事項:

  • ALB 用 EIP × 2 (175.41.232.218 / 54.92.6.226) は amazon-elb 管理で release 不可 → ALB taki 自体を削除しない限り動かせない(taki は他4アプリ用なので削除しない、許容)
  • 旧 ACM 証明書 (arn:aws:acm:.../4eec...) の SAN 確認は未実施。他ホスト名 (baseballnext / footballnext / sposhin / machiga) も含まれてる前提で残置
  • 認証機構の追加(チート対策)は v1 と同じく未実装

2026-04-27 19:43 時点のスナップショット

🎯 v2 100% 稼働中、v1 1号機を停止。翌朝 (12h 後) のタイマー通知で観測再開予定。

  • v2 が schulte.thomsonsapp.com の全トラフィックを処理中
  • v1: 1号機 (Schulte_maintenance タグで stopped)、2号機 (元から stopped)
  • ALB schulte TG: 1号機 deregister 済み、流入ゼロ
  • ALB taki: schulte リスナールールはまだ残置、流入ゼロ
  • DNS: alb-v1 (Weight 0) 残置、lightsail-v2 (Weight 100) 稼働中
  • dev-timer #3 が 2026/4/28 7:42 に発火予定

次にやること(翌朝確認後の撤退仕上げ): DNS alb-v1 削除 / ALB taki から schulte リスナールール削除 / ALB schulte TG 削除 / Lambda 整理 / EC2 1号機・2号機 terminate。

2026-04-27 19:33 時点のスナップショット

🎯 本番 100% v2 切り替え完了、安定稼働中。残るは v1 撤退作業のみ。

  • schulte.thomsonsapp.com → 100% Lightsail (52.68.176.128)
  • ALB schulte TG への流入: 19:26 以降 0 RPM
  • v2 トラフィック: avg 0.08 RPS、5xx ゼロ、メモリ available 424MB
  • 監視 (senrigan-observer) の URL は /healthcheck に変更済み
  • ALB Alias レコードは Weight 0 で残置(即時切り戻し用)

次にやること: 観測 → ALB 1号機 deregister → EC2 停止 → Lambda 整理 → terminate。

2026-04-27 18:51 時点のスナップショット

本番ドメイン切り替えの準備が完全に整った。次セッションで切り替え発射。

  • Lightsail で v2 が完全稼働、両ドメイン (v2-test / schulte) の SSL 証明書取得済み
  • Nginx は HTTP/HTTPS 両方受け、各 server_name で別々の証明書を読む構成
  • curl --resolve で本番 DNS を変えずに HTTPS 動作確認済み
  • 切り戻し用情報は DEVLOG に inline 記録

重要な定数: schulte zone Z035498717N5DORY02AM0、現 Alias Target dualstack.taki-879563088.ap-northeast-1.elb.amazonaws.com.、ALB Hosted Zone Z14GRHDCWA56QT、Lightsail IP 52.68.176.128

次にやること: Route53 Weighted Routing で v2 を 10% → 50% → 100% に段階切り替え発射。

2026-04-27 18:07 時点のスナップショット

v2 が https://v2-test.thomsonsapp.com で完全稼働。残るは本番ドメイン切り替えのみ。

  • v2 全 8 エンドポイントが HTTPS 経由で本番 RDS / 本番 S3 と疎通確認済み
  • v2-test.thomsonsapp.com は A レコードで Lightsail (52.68.176.128) 向き、TTL 60秒
  • Let's Encrypt 証明書(CN=v2-test.thomsonsapp.com、自動更新)
  • Nginx は HTTP/HTTPS 両方受ける構成

次にやること: 本番ドメイン切り替え準備(schulte 用証明書、Nginx schulte server block、切り戻し情報整備)。

2026-04-27 17:51 時点のスナップショット

Lightsail で v2 が本番相当環境で完全稼働。残るは SSL + DNS 切り替えのみ。

  • v2 全 8 エンドポイントが本番 RDS / 本番 S3 に対して動作確認済み
  • Lightsail 構成: micro_3_0/1GB/Ubuntu 24.04、静的 IP 52.68.176.128、systemd + Nginx + 専用 IAM
  • v2 最新コミット b6fedd2(pydantic-settings の extra='ignore' 修正)
  • 本番 v1 は schulte 1号機のみ稼働、2号機は停止中

次にやること: ファイアウォール 443 開放、SSL 取得、DNS 切り替え準備、段階切り替え。

2026-04-27 17:32 時点のスナップショット

Lightsail インスタンス上で v2 が動作確認済み。次は systemd 化 + Nginx + S3 認証。

  • Lightsail インスタンス作成済み(schulte-server-v2, micro_3_0/1GB, Ubuntu 24.04)
  • 静的 IP: 52.68.176.128、VPC Peering 有効、RDS SG 許可済み
  • インスタンス内: uv + Python 3.13 + git + gh セットアップ済み
  • リポジトリ clone 済み + .env 配置済み
  • uvicorn 動作確認: /healthcheck/db で MySQL 8.4.8 接続, /v1/sync で本番データ取得確認

次にやること: systemd service 化、Nginx リバプロ、IAM ユーザー作成、uploadRanking* 本番確認。

2026-04-27 17:06 時点のスナップショット

v2 (Python/FastAPI) の v1 同等機能 API 全 8 本 実装完了。次は Lightsail へのデプロイ。

  • 実装済みエンドポイント (本番 RDS / 本番 S3 で全ケース動作確認済み):
    • GET /healthcheck / GET /healthcheck/db
    • POST /v1/sendScore
    • POST /v1/createPlayer
    • POST /v1/updatePlayerName
    • GET /v1/sync
    • GET /v1/downloadRankingTsv/{mode}/{term}
    • GET /v1/uploadRankingTsvToS3/{mode}/{term}/{delay}
    • GET /v1/uploadRankingScoresAndPlayersJsonData/{mode}/{term}/{delay}
  • v2 最新コミット: dfac086
  • 開発時の S3 認証は ~/.aws/credentials の thomson-ik プロファイル経由

次にやること: Lightsail インスタンス構築、本番用 IAM 作成、切り替え手順設計。

2026-04-27 16:51 時点のスナップショット

v2 (Python/FastAPI) の DB 系 API 6本実装完了。残るはバッチ系2本のみ。

  • 実装済みエンドポイント (本番 RDS で全ケース動作確認済み):
    • GET /healthcheck / GET /healthcheck/db
    • POST /v1/sendScore
    • POST /v1/createPlayer
    • POST /v1/updatePlayerName
    • GET /v1/sync
    • GET /v1/downloadRankingTsv/{mode}/{term}
  • v2 最新コミット: ad714bc (https://github.com/ichirokisanuki/schulte-server-v2/commit/ad714bc)
  • 本番は schulte 1号機のみ稼働、2号機は停止中(AppName=Schulte_maintenance)

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

  1. バッチ系2本実装: uploadRankingTsvToS3, uploadRankingScoresAndPlayersJsonData(aioboto3 で S3 連携)
  2. v2 を Lightsail に新規構築してデプロイ動作検証
  3. ALB taki から schulte TG を外す or DNS 切り替えで v2 へトラフィック寄せる手順設計

未決事項:

  • S3 書き込み認証情報をどう持たせるか(次セッション最初の論点)
  • v2 の動作確認方針: 現状は本番 RDS にテスト送信→削除でやっているが、ローカル MySQL を Docker Compose で立てる方が綺麗(優先度低)
  • 認証機構: 現状 v1 と同じく認証なし。チート対策は v2 完全切り替え後に考える

2026-04-27 16:41 時点のスナップショット

v2 (Python/FastAPI) のコア実装フェーズ。DB 接続 + 主要 API 2本まで完了。

  • DB スキーマを実 RDS から吸い出し済み (schulte-server-v2/schema/init.sql)
  • v2 に DB 接続実装済み (aiomysql + SQLAlchemy 2.x async)、本番 RDS (MySQL 8.4.8) で疎通確認
  • 実装済みエンドポイント:
    • GET /healthcheck
    • GET /healthcheck/db
    • POST /v1/sendScore (本番 RDS で4ケース動作確認済み、テストレコードは削除済み)
    • GET /v1/sync (本番 RDS で 37 players / 500 scores 取得確認)
  • v2 最新コミット: 3480683 (https://github.com/ichirokisanuki/schulte-server-v2/commit/3480683)
  • 本番は schulte 1号機のみ稼働、2号機は停止中(AppName=Schulte_maintenance)

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

  1. 残り API 3本実装: createPlayer, updatePlayerName, downloadRankingTsv(DB 操作のみで完結)
  2. バッチ系2本実装: uploadRankingTsvToS3, uploadRankingScoresAndPlayersJsonData(aioboto3 で S3 連携、IAM 認証情報の準備要)
  3. v2 を Lightsail に新規構築してデプロイ動作検証
  4. ALB taki から schulte TG を外す or DNS 切り替えで v2 へトラフィック寄せる手順設計

未決事項:

  • v2 の動作確認方針: 現状は本番 RDS にテスト送信→削除でやっているが、ローカル MySQL を Docker Compose で立てる方が綺麗。優先度は低いがいつか整備したい
  • v2 から S3 への書き込み認証: Lightsail 上で動かすときの IAM ロール / アクセスキーをどう持たせるか(環境変数 / IAM Role for EC2 相当の機能 / アクセスキー直書き)
  • 認証機構: 現状 v1 と同じく認証なし。チート対策は v2 完全切り替え後に考える

2026-04-27 15:50 時点のスナップショット

v2 (Python/FastAPI) 開発を本格スタートしたところ。

  • schulte-server-v2 リポジトリ作成完了 → https://github.com/ichirokisanuki/schulte-server-v2
  • ローカル: ~/cdev/schulte-operation/schulte-server-v2/
  • 最小スケルトン (/healthcheck) 動作確認済み
  • 本番は schulte 1号機 (i-039d46fb13dc9f81d) のみで継続稼働中
  • 2号機 (i-06541bacc1fd226ea) は停止中、AppName タグ Schulte_maintenance で Lambda 監視対象から外している
  • ALB taki は schulte 含む5アプリ共有 → v2 移行時に経路変更が必要

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

  1. DB スキーマを実 RDS (footballnext) から SHOW CREATE TABLE で吸い出す
  2. v2 に DB 接続 (aiomysql + SQLAlchemy async) を実装
  3. v2 に sendScore エンドポイント実装
  4. v2 に sync エンドポイント実装
  5. 残りエンドポイント (createPlayer / updatePlayerName / downloadRankingTsv / バッチ系) 実装
  6. v2 を Lightsail に新規構築してデプロイ動作検証

未決事項:

  • v1 から v2 への切り替え方式: DNS 切り替え or ALB ルール変更 で v2 にトラフィック寄せる流れになる想定だが、具体手順は要設計(特にダウンタイムをどう扱うか)
  • 2号機の今後: 永久停止 or v1 のまま塩漬け。本番1台運用で問題なさげなので、当面は停止継続で良さげ
  • v1 のソースコード (CodeCommit) を GitHub 移行する話は v2 で書き直す方針になり優先度が大きく下がった。完全撤退時に CodeCommit ごと閉じる運用で良さそう

2026-04-27 13:11 時点のスナップショット

プロジェクトの初期セットアップ段階。

  • リポジトリ直下に schulte-server/ schulte-unity/ を物理配置済み(.gitignore で除外、別管理)
  • CLAUDE.md にプロジェクト概要を記入済み(Unity + PHP / EC2 nano × 2台 + LB / RDS 間借り)
  • 直近ロードマップ3項目を .devnotes/ROADMAP.md 「今月」に登録済み
    1. EC2 nano × 2台 → Lightsail 1台への縮小
    2. schulte-server を CodeCommit → github.com に移行
    3. schulte-unity を CodeCommit → github.com に移行(要検討)

次にやること: 上記3項目のうちどれから着手するか方針決定。着手する場合の準備として、CodeCommit 側のリポジトリ実態(サイズ・ブランチ)を把握する必要がある。

未決事項: schulte-unity のリポジトリサイズが大きい場合、GitHub に上げるかどうか(LFS 利用 / 一部除外 / 移行見送りなどの選択肢)。

ROADMAP(計画)

ロードマップ

今週

  • Google Play の 8/31 期限対応(targetSdk 36 / Billing 8.3.0 / appCategory=game)
  • 署名済み AAB (versionCode 7 / 1.2.3) を内部テストに配信
  • SSL 証明書失効の復旧(certbot を dns-route53 → webroot に変更)
  • 監視に証明書残日数チェックと NG 継続時の再通知を追加(senrigan-observer 側)
  • 実機で内部テスト版を検証(スコア送信・ランキング・Android 16 の UI と画面向き)
  • 製品版へ昇格し審査提出(vc7 / 1.2.3)
  • ネイティブデバッグシンボルを Play Console の画面からアップロード(vc7 分・完了)
  • 審査通過後にアラート3件が消えたか確認
  • Unity の Create symbols.zip を Public にする(CIBuild の -ciSymbolsPublic で AAB と同時生成、vc8 から)
  • 次回リリース: score の文字列化を CultureInfo.InvariantCulture に修正(コード修正済み・未ビルド)
  • ランキングをプレイヤーごとの自己ベストのみに変更(サーバーのみ、2026-09-08 デプロイ済み・アプリ更新不要)
  • 結果画面: ランキング常時表示 + 自分の行を赤/薄い赤で区別(実機確認済み)
  • iOS 1.2.3 (1) を TestFlight にアップロード(2026-09-09)
  • TestFlight で確認 → schulte-unity をコミット・タグ iOS1.2.3 → App Store 審査提出(1.2.3 公開済み)
  • Android 1.2.4 (vc8) を同じコードでビルド(symbols.zip を Public にしてから)→ 内部テスト → 製品版(vc8 は 16KB 警告で止まり、共有クラッシュを直した vc9 で申請、2026-09-25)
  • Play の 16KB 警告の原因調査 → NativeShare の共有クラッシュ(FLAG_MUTABLE 欠落)を 1 クラスのパッチで修正(2026-09-25)
  • Android 1.2.4 (vc9) の審査結果を確認 → 公開後にタグ Android1.2.4(公開・タグ付け済み)
  • 公開 3〜4 日後に vitals で NativeShare のクラッシュが 1.2.4 で消えたか確認(dev-timer #85。2026-09-29 に確認、vc9 で再発なし・#85 は done)
  • SNS の購読に普段使いの ichirokisanuki@gmail.com を追加(承認済み)
  • スコアの異常値をモード別下限で sendScore が弾く(4x4 2 秒 / 5x5 4 秒 / 6x6 7 秒 / 7x7 15 秒、2026-09-11 デプロイ済み 8c7ffac)
  • 閾値未満の既存行を del=1 にする(トムソン指示、2026-09-11 実施、10,182 行。閾値内の偽造 ID は BAN しない)

今月

  • 本番 DNS 切り替え発射: Route53 Weighted Routing で v2 を 10% → 50% → 100% に段階切り替え
  • 監視サービス (senrigan-observer) の URL を /healthcheck に変更
  • 翌朝 (2026/4/28 7:42 の dev-timer 通知) で v2 安定稼働確認
  • ALB schulte TG から 1号機を deregister
  • 1号機 EC2 停止 (AppName=Schulte_maintenance タグで Lambda 自動再起動防止)
  • DNS の alb-v1 Weight 0 レコード削除
  • ALB taki から schulte リスナールール + schulte TG 削除
  • Lambda TCM_observeALB_* の Schulte 関連分削除
  • 旧 schulte EC2 (1号機・2号機) を terminate
  • v2 リソース増強 (swap 1GB + uvicorn workers=2)
  • 本番 RDS (footballnext / baseballnext) を PubliclyAccessible=false 化(月 -$7.3)
  • 24 時間ランキングをイベント駆動で即時更新(sendScore 後にバックグラウンドで daily 再生成、cron は 30 分の保険に)。2026-09-11 デプロイ済み(f4b0a67)
  • 2 週間後に vitals で NativeShare のクラッシュ再発なしを再確認(dev-timer #89。2026-10-13 に確認、vc9 は 60 日で issue 0 件・日次クラッシュ率 12 日分すべて 0.00%。#89 は done)

今四半期

  • schulte-unity を CodeCommit → github.com に移行(リポジトリ容量次第)
  • (別リポジトリ ~/cdev/common-php-my-admin 側で)SSH トンネル経由 phpmyadmin の整備
  • (オプション)kizuna RDS も PubliclyAccessible=false 化(月 -$3.65 追加削減)

いつか

  • インタースティシャル広告が閉じられない事象の調査(AppLovin のクリエイティブ / 頻度設定)

  • 認証機構の追加(チート対策)— 次回アプデで検討。段階案と移行策は WIP.md 参照。 本命はセッショントークン + 「証明付きのスコアだけランキング掲載」

  • IP ベースのレート制限

  • レスポンスタイム計測やエラー集計(メトリクス)の整備

  • v2 のテスト整備(pytest + httpx)

  • ローカル開発用 MySQL を Docker Compose で立てる

  • v2 のログを CloudWatch Logs / Loki などに集約(現状 systemd journal のみ)

  • 旧 ACM 証明書 (arn:aws:acm:.../4eec...) の SAN 確認 → schulte 用 SAN だけ削れるなら削除

  • NativeShare をプラグインごと上流最新(v1.5.5)へ更新するか検討(2026-09-25 は NativeShareBroadcastListener だけパッチ)

DECISIONS(意思決定)

意思決定記録

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


2026-09-29: 他 3 アプリの NativeShare 修正は、各アプリの次回アップデートのときに直す

背景: schulte の共有クラッシュ(NativeShare の FLAG_MUTABLE 欠落)は、1.2.4 (vc9) で再発していないことを vitals で確認できた。勉強タイマー・kowai-best・断食タイマーにも同じ未修正の NativeShare が残っていて、どれも vitals のクラッシュ 1 位。

決定: 3 アプリとも、修正は各アプリの次回アップデートのときに行う。今回は着手しない。

理由: You の判断(「別のアプデのときでいい」)。今すぐ 3 アプリを個別に修正して出す代わりに、どのみち出す次回アップデートに同梱する。

2026-09-25: 16KB ページサイズ警告は「このまま続行」で押し切らず、原因のクラッシュを直した vc9 で申請し直す

背景: 1.2.4 (vc8) の製品版申請で、Play Console が「16 KB ページサイズに対応していない可能性」(ラボの 16KB 端末でクラッシュ検出)を出し、審査に送れなかった。「このまま続行」も選べた。

決定: 押し切らずに原因を調べ、16KB 非対応ではなく共有ボタンの既存クラッシュ(NativeShare)だと突き止めて修正した。vc8 はアップロード済みで再利用できないため、versionName は 1.2.4 のまま versionCode 9 で申請し直す。

理由: Android 12+ で共有ボタンを押すと 100% 落ちる実害のある不具合で、製品版 1.2.3 でも vitals のクラッシュ 1 位だった。押し切っても次回以降の申請で同じ警告が出る。1.2.4 は未公開なので versionName は変えない。

2026-09-25: NativeShare はプラグイン全体を更新せず、問題のクラス 1 つだけを差し替えたパッチで直す

背景: 同梱の NativeShare.aar(2021-05 ビルド)の NativeShareBroadcastListener が PendingIntent に FLAG_MUTABLE を付けていない。上流の最新 v1.5.5 は修正済み。

決定: 上流と同じ修正(SDK_INT >= 31 で FLAG_MUTABLE)を入れたクラスだけをコンパイルし、既存 AAR の classes.jar 内の同名クラスを置き換える。ソースは patches/nativeshare-flag-mutable/ に残す。プラグイン全体の v1.5.5 への更新は見送る。

理由: 変更が Android の 1 クラスに閉じ、iOS 側や C# の API に影響しないので、すぐ出せて回帰確認も共有ボタンだけで済む。v1.5.5 への全体更新は .unitypackage の取り込みと iOS を含めた再確認が要る。全体更新はいつか検討する。

2026-09-11: 閾値未満の既存行は del=1 で除外するが、閾値内に収まっている偽造 ID は BAN しない

背景: 閾値未満の既存行を除外したところ、全期間トップが各モードとも閾値ちょうど上(2.0 / 4.00016 / 7.0003 / 15.0007)になった。除外したのと同じ数人の ID が閾値直上に数百行ずつ持っており、行単位では切れない。is_ban=1 ならランキングから即座に消せる。

決定: トムソンの指示で閾値未満の既存 10,182 行は del=1 にする(対象 id は Lightsail に保存して戻せる状態)。閾値内に収まっている記録は、送信者が偽造 ID と分かっていても BAN しない。

理由: 閾値は「これ未満は異常値」として公開・共有したルールであり、収まっている記録を送信者の素性で弾くとルールが二重になる。ID 単位の判定は証拠の強弱が人によって違い、誤って正規の上位プレイヤーを消すリスクもある。閾値直上の偽造は本命のセッショントークン方式で構造的に消す方針を変えない。

2026-09-11: スコアの異常値はモード別の下限(4x4 2 秒 / 5x5 4 秒 / 6x6 7 秒 / 7x7 15 秒)とし、保存せず成功応答で黙って捨てる

背景: トムソンから「各モードの異常値を求めて sendScore で弾きたい」と依頼された。2026-08-30 に下限チェックは対症療法だと整理していたが、明白な偽造クラスタ(6x6 5.0〜6.5 秒、7x7 12〜15 秒)がランキングに居座っている状況は即座に止めたい。

決定: 本番の全期間データから、正規プレイヤーの最速帯(4x4 3 秒台 / 5x5 6 秒台 / 6x6 8 秒台 / 7x7 16 秒台)より十分下で偽造クラスタを含む位置に下限を置く。You の原案(6x6 5 秒 / 7x7 12 秒)は偽造クラスタが残るため 6x6・7x7 を引き上げた。下限未満は DB に保存せず、送信元には isSuccessed: true と playerId を返す。del=1 で保存する案は採らない。既存行の掃除は保留。

理由: 「閾値未満は物理的に不可能」と言える位置に置けば正規ユーザーを巻き込まず、マスが増えるほど 1 タップあたりの探索時間が伸びることを速度換算で確認したうえで決めた。成功応答にするのは偽造者に閾値の位置を探らせないため。証拠は journal のログで足りるので DB に残す必要はない。これはあくまで対症療法で、本命のセッショントークン方式は変えない。

2026-09-11: 24 時間ランキングは sendScore の 24 時間ベスト更新をトリガーに即時再生成し、cron は 30 分ごとの保険に落とす

背景: 結果画面がランキング常時表示になり、プレイ直後に自分の行が載っていないと不自然になった。2026-09-09 の試算で負荷は問題なしと分かっていた。

決定: sendScore で保存後、そのプレイヤーの 24 時間内ベストが更新された場合だけ当該モードの daily(TSV + JSON)をバックグラウンドで再生成して S3 に置く。同一モードの再生成中に来た要求は保留にまとめて終了後 1 回だけ再実行する。daily の cron は 5 分ごと → 30 分ごとにし、weekly / total は据え置く。

理由: 24 時間一覧は各モード数十〜200 人で上限に達しないため、ベスト更新=ランクインと見なせて判定が 1 クエリで済む。全送信で再生成しても負荷は許容内だが、ベスト更新に絞れば S3 PUT は従来の cron より減る。cron を残すのはバックグラウンドタスクが落ちた場合の保険。アプリ更新なしで全バージョンに効く。

2026-09-09: 結果画面はランキングを常に表示し、自分の行の色だけで今回の結果を伝える

背景: サーバー側を 1 人 1 行にしたことで、結果画面が「S3 のスナップショット + 端末で差し込んだ今回の行」を合成する構造のままだと、自分の過去ベストと今回の 2 行が並ぶ。「ベスト更新時だけランキング、それ以外は自分の記録カード」案は、順位が今回の記録に付いたものではないのに大きく出る矛盾があった。「ランキング + 説明 1 行」案は文言を何度変えても余計に見え、ランキング画面と見た目が揃わなかった。

決定: 結果画面はランキングを常に表示する。自分の行は 24 時間ベストの 1 行だけ。今回がベストとして載ったときは赤、今回は届かず今日のベストが載っているだけのときは薄い赤にする。上位 500 人の圏外は自分の行を出さず一覧の末尾 7 行を見せる。説明の文言は一切出さない。

理由: 「自分の行があるか」「その時間が今回の記録と同じか」「色が濃いか薄いか」で 6 状態すべてを区別でき、文言のローカライズも要らない。ランキング画面(more)と同じ見え方になり、画面が増えた印象を与えない。判断材料は実機スクショと 6 状態を並べたモックで、複数案を実際に見比べて絞り込んだ。


2026-09-08: ランキングはプレイヤーごとの自己ベストのみを掲載する

背景: ランキングはスコア行をそのまま速い順に並べていたため、同じ人が何行も並んでいた。実測では全期間 6x6 の上位 1000 行が 5 人で占められ、24時間の 4x4 も 897 行中 105 人しかいなかった。偽造スコアを送る人物が何百行も占有する見え方にもなっていた。

決定: サーバーの _fetch_ranking_rows() でプレイヤーごとの最小スコアに集約し、1 人 1 行にする。併せて is_ban = 1 のプレイヤーを除外する。全期間集計のため modeN_scores に (del, player_id, score) のインデックスを追加する。cron の頻度は変えない。

理由: クライアントは S3 の静的ファイルを描画しているだけなので、サーバー修正だけで全バージョン・全画面に即座に効き、ストア審査が不要。順位が「プレイ回数」ではなく「人」の順位になり、上位を 1 人が独占する構造が消えるため、スコア偽造対策で検討していた「ランキングの位置づけを下げる」案の一部も同時に達成できる。インデックス追加は全期間集計が 16 秒かかるのを避けるためで、追加後は約 1 秒に収まった。


2026-08-30: ロケール由来のスコア送信失敗は、クライアントではなくサーバー側で受け止める

背景: 小数点にカンマを使うロケール(メキシコ・スペイン・フランス等)のユーザーが 25,74 の形でスコアを送っており、FastAPI の float パースで 422 になっていた。該当地域のユーザーは毎回失敗しており、24時間で全送信の約2.8%が失われていた。根本原因はクライアントの "" + score がカルチャ依存の文字列化をしている点。

決定: まずサーバー側で score を文字列として受け、カンマを小数点として解釈する。クライアント側の修正は次回リリースに回す。

理由: クライアントだけを直しても、旧バージョンを使い続けるユーザーは次回更新まで救えず、更新しないユーザーは永久に救えない。サーバー側で受け止めれば既存の全バージョンが即座に復旧する。サーバー変更は parse_score() の追加のみで影響範囲が小さく、桁区切りと小数点が混在する曖昧なケースは従来どおり拒否するため、誤ったデータが入る危険もない。クライアント側の修正は正攻法として別途行うが、緊急性は無くなる。


2026-08-28: certbot の認証方式を dns-route53 から webroot (HTTP-01) に変更する

背景: Let's Encrypt 証明書が 2026-07-26 に失効し、スコア送信が33日間停止した。certbot.timer は毎日動いていたが、dns-route53 が使う AWS 認証情報を root が持っておらず、Lightsail のインスタンスロール(Route53 権限なし)にフォールバックして AccessDenied で失敗し続けていた。.env の IAM ユーザーも S3 専用で、しかも別 AWS アカウント。

決定: 認証方式を webroot(HTTP-01)に変更する。nginx の 80番ブロックに /.well-known/acme-challenge/ の location を足し、/var/www/certbot を webroot にする。

理由: 80番は外部から到達可能で、ワイルドカード証明書も使っていないため DNS-01 である必要がない。webroot にすれば AWS 認証情報への依存が完全に消え、今回の失敗モード自体が成立しなくなる。代替案の「IAM に Route53 権限を付与」は、ホストゾーンの所在確認・ポリシー作成・certbot への認証情報の受け渡しと工程が多く、依存関係も残ったままになるため採らなかった。


2026-08-28: Unity IAP は 5.x ではなく 4.15.1 に上げる

背景: Google Play の 2026/8/31 期限で Billing Library 8 以上が必須になった。現状は com.unity.purchasing 4.12.2(Billing 6.2.1)。期限まで残り3日だった。

決定: com.unity.purchasing を 4.15.1 に更新する(5.x には上げない)。

理由: 4.15.1 は Billing Library 8.3.0 を同梱しており要件を満たすうえ、4.x の旧 API(IStoreListener / IStoreController / UnityPurchasing.Initialize)のままなので PurchasingManager.cs を1行も書き換えずに済む。実際に3段階のビルド検証すべてでエラー0だった。5.x は UnityIAPServices ベースの新設計で API が総取っ替えになり、3日で安全に移行できる規模ではない。4.13.0 の破壊的変更(GooglePlayProrationMode → GooglePlayReplacementMode)もプロジェクト内で未使用のため影響なし。


2026-04-28: 本番 RDS は PubliclyAccessible=false に統一(コスト削減 + セキュリティ)

背景: 撤退作業の延長で AWS account 内の EIP 一覧を確認した結果、4個の Elastic IP が RequesterManaged で残存していた。うち 2個は ALB 用 (release 不可)、2個は RDS 用 (amazon-rds 管理) で、PubliclyAccessible=true ゆえに保持されていることが判明。2024-02-01 から AWS は auto-assigned Public IPv4 にも課金するようになっており、Public 維持のコストが顕在化していた。

決定: 本番 RDS (footballnext / baseballnext) は PubliclyAccessible=false に統一する。インターネット経由での到達は不要(接続元はすべて同 VPC 内 EC2 / VPC Peering 経由 Lightsail)。phpmyadmin など外部から直接接続していたツールは SSH トンネル方式へ切り替える。

理由:

  • 2 RDS で月 $7.3 のコスト削減(EIP × 2 が amazon-rds 管理で自動解放される)
  • DNS 解決でも RDS のホスト名が世界から見えなくなり、攻撃面の縮小
  • Public 接続点を持たない方が、SG ミス時のフォールバックも安全
  • phpmyadmin 系は SSH トンネル経由の方が現代的な運用

2026-04-28: ALB 用 EIP (amazon-elb RequesterManaged) は触らない方針

背景: ALB taki にぶら下がる Elastic IP × 2 (175.41.232.218 / 54.92.6.226) は RequesterManaged=true、RequesterId=amazon-elb で AWS が自動管理。release-address を試しても OperationNotPermitted で拒否される。手動解放の手段は ALB 自体を削除するしかない。

決定: ALB 用 EIP は触らない。ALB taki は schulte 以外の4アプリで継続使用するため削除しない。

理由:

  • ALB taki を削除すると baseballnext / footballnext / machiga / sposhin の経路が壊れる
  • ALB を残す限り EIP も AWS が自動でメンテし続けるので、release できないことに実害はない
  • ALB の月額に EIP 課金分は事実上含まれており、別計上で動かせない構造

2026-04-28: v2 のリソース増強 (swap 1GB + uvicorn workers=2)

背景: 100% v2 切り替え後の長期運用に向けて、深夜帯の安定性をリスク分析した。Lightsail micro_3_0 (1GB RAM) は swap なし、uvicorn は single worker で起動していた。OOM 時のフェイルセーフがなく、長時間処理がリクエストをブロックする構成。

決定: Lightsail インスタンスに以下を追加。

  • /swapfile 1GB を作成し /etc/fstab で永続化
  • systemd unit の ExecStart に --workers 2 を追加して uvicorn を 2 worker に

理由:

  • swap は OOM Killer 発動前のセーフティネット。physical メモリが枯渇しても緩やかに劣化(即座に kill されない)
  • workers=2 なら長時間処理(uploadRanking* 等)の最中でも他リクエストが詰まらない
  • 追加コストはゼロ(既存 Lightsail プラン内のリソース)
  • メモリ available 356MB 確保できているので実害なし

2026-04-28: phpmyadmin 接続は SSH トンネル方式に切り替える(本番 RDS Public 戻しはしない)

背景: RDS Private 化の副作用で ~/cdev/common-php-my-admin (mydb.ikapps.com) から footballnext / baseballnext へ接続不可になった。Public に戻す案 (月 +$7.3) と、SSH トンネル経由でアクセスする案を比較。

決定: SSH トンネル方式を採用する。多目的サーバー (Lightsail) から cron インスタンス (52.194.87.168) を踏み台にして、systemd 管理の常時 SSH トンネルを張る。config.inc.php の host を 127.0.0.1:13306 127.0.0.1:13307 に変更。

理由:

  • Public に戻すと月 $7.3 のコストが復活、セキュリティ面でも逆行
  • SSH トンネルは追加コストゼロ、踏み台 (cron インスタンス) は既に存在
  • RequesterManaged 解放後の状態を維持したまま、phpmyadmin の利便性を取り戻せる
  • 同じ仕組みで kizuna も Private 化可能(オプション、追加で月 -$3.65)

実作業の場所: 別リポジトリ ~/cdev/common-php-my-admin 側で対応。引き継ぎは 引き継ぎ.md に記載済み。


2026-04-27: 監視サービスの死活監視 URL は v2 標準の /healthcheck に統一

背景: schulte.thomsonsapp.com/v1/healthcheck は v1 のみのエンドポイント (PHP datetime() 文字列を返す)。v2 では /v1/healthcheck を実装せず /healthcheck ({"status":"ok"} を返す) を持っている。本番切り替え中に外部監視サービス senrigan-observer/0.1 が /v1/healthcheck を死活監視していることが判明し、振り分けされた瞬間に 404 でアラートが鳴る問題に直面した。

決定: 監視 URL を /v1/healthcheck から /healthcheck に変更する。v2 に /v1/healthcheck 互換実装を追加するのではなく、v1 撤退完了に向けて素直に v2 標準のエンドポイントに監視も寄せる方針。

理由:

  • 100% v2 切り替えと同時に v1 が完全停止するなら、/v1/* 互換性を v2 で保つ意味は薄い
  • 監視 URL は外部サービス側で書き換えるだけで完結(v2 のコード変更なし)
  • /healthcheck は v2 標準のシンプルな名前で、新規 API 設計とも自然

2026-04-27: 切り戻し用に ALB Alias レコードを Weight 0 で残置する

背景: 100% v2 切り替え後、ALB へのトラフィックは完全にゼロ (CloudWatch メトリクスで確認)。ALB Alias レコードを削除すれば Route53 構成はシンプルになるが、即時切り戻しの選択肢を失う。

決定: ALB Alias レコード (alb-v1) を Weight 0 のまま残置する。十分な観測期間(少なくとも数時間〜1日)を経て安定確認できたら、その後で削除する。

理由:

  • Weighted Routing で Weight 0 のレコードは選択されないため、実害ゼロ
  • 万一 v2 で何か問題が出ても、Weight を 100/0 から 0/100 に戻すだけで即時切り戻し可能(TTL 60秒で復旧)
  • 削除コストは1分なので、撤退時にいつでもクリーンアップできる

2026-04-27: v1 → v2 切り替えはサブドメイン事前検証 + Weighted Routing で段階切り替え

背景: v2 の本番投入時、schulte.thomsonsapp.com の DNS A レコードを直接 Lightsail 静的 IP に切り替えると、リアルユーザーへの影響が一括で発生する。レスポンス形式の微妙な差異や負荷耐性の不明なまま全切り替えするのはリスクが高い。

決定: 以下の3段構えで切り替えを進める。

  1. 事前検証: v2-test.thomsonsapp.com というテスト用サブドメインを切り、Lightsail に向ける。Let's Encrypt 証明書も別途取得し、HTTPS で全エンドポイント動作確認する(本番影響ゼロ)。
  2. TTL 短縮: 本番 schulte.thomsonsapp.com の TTL を切り替え数日前に短縮(現在 ALB の Alias なのでそもそも TTL 効かない可能性、要確認)。
  3. 段階切り替え: Route53 Weighted Routing を使い、v2 への割合を 10% → 50% → 100% と段階的に増やす。各段階で観測し、問題があれば即 v1 に戻す。

理由:

  • 「全か無か」の切り替えは個人開発でもリスクが大きすぎる
  • Weighted Routing は Route53 標準機能で追加コストなし
  • 切り戻しは DNS レコードの重みを 0 に戻すだけ、TTL 60秒なら 1分以内で復帰可能
  • サブドメイン検証は本番影響ゼロでフルテストできる、典型的な Blue/Green 戦法に近い

2026-04-27: SSL 証明書は Let's Encrypt + DNS-01 challenge (certbot-dns-route53)

背景: v2 は HTTPS 必須(Unity クライアントが https://schulte.thomsonsapp.com/v1/... で叩いてくる)。ALB にある既存 ACM 証明書は Lightsail で流用できないため、Lightsail 側で独自に証明書を持つ必要がある。HTTP-01 challenge と DNS-01 challenge から方式選択。

決定: Let's Encrypt + DNS-01 challenge を採用。certbot-dns-route53 plugin を使い、専用 IAM ユーザー schulte-server-v2-cert のクレデンシャルで Route53 を操作する。

理由:

  • HTTP-01 はドメインが既に Lightsail に向いている必要があり、本番ドメインの切り替え前には使えない
  • DNS-01 はドメインの実所属に依存せず、Route53 にレコードを置くだけで検証完了
  • 本番ドメイン (schulte.thomsonsapp.com) も切り替え前に証明書を準備しておけるので、切り替え時の手間が減る
  • IAM は route53:ChangeResourceRecordSets を arn:aws:route53:::hostedzone/<zone> 限定の最小権限
  • certbot は Ubuntu apt 経由でインストールすると systemd timer による自動更新が組み込まれる

2026-04-27: v2 用 IAM ユーザーは最小権限 (s3:PutObject + PutObjectAcl on rankings/* のみ)

背景: v2 の S3 連携 (uploadRanking* バッチ) を本番 Lightsail 環境で動かすにあたり、使う IAM ユーザーの権限設計が必要だった。広い権限のユーザー (ichirokisanuki のような S3FullAccess 持ち) を流用する案もあったが、本番運用ではキー漏洩時の被害最小化を優先。

決定: 専用ユーザー schulte-server-v2-s3 を新規作成し、インラインポリシー S3PutObjectRankings で s3:PutObject + s3:PutObjectAcl を arn:aws:s3:::schulte-thomsons/rankings/* のみに許可する。それ以外(DELETE、GetObject、他バケット、他サービス)は一切許可しない。

理由:

  • 万一キーが漏洩しても、schulte-thomsons バケットの rankings/ 配下の上書きしかできず、被害は v1 cron が次の周期で上書きすれば自動修復可能
  • 削除権限なし → 既存ファイルを消されることはない
  • 他バケット・他サービス不可 → 横展開のリスクなし
  • 個人開発でも本番運用での衛生的な分離を選んだ

2026-04-27: pydantic-settings の Settings には必ず extra='ignore' を明示する

背景: v2 を Lightsail で systemd 化した際、.env に AWS_ACCESS_KEY_ID 等を追加したら起動が失敗。原因は pydantic-settings v2 のデフォルトが extra='forbid' で、Settings 未定義の環境変数があるとバリデーションエラーになる仕様だった。さらに、エラーメッセージ input_value='...' に secret access key の生値 が含まれて journalctl に出力され、ローカルターミナル / Claude session context に露出した(即無効化で実害なし)。

決定: 全プロジェクトで Settings クラスに model_config = SettingsConfigDict(..., extra="ignore") を必ず明示する。これにより、.env に Settings 未定義のキー(AWS 認証情報など、SDK が直接環境変数から拾うもの)を書いてもエラーにならず、かつ秘密情報がエラーメッセージに混入しない。

理由:

  • pydantic のエラーは入力値を input_value=... でそのまま出すため、秘密情報を含む環境変数では深刻な情報漏洩源になる
  • aioboto3 / boto3 は AWS_* を環境変数から自動で拾うので、Settings で受け取る必要はない
  • 「未定義の env は無視する」が現代的な API 設計でも一般的な振る舞い

2026-04-27: Lightsail プランは micro_3_0 ($7/月, 1GB RAM, dual stack)

背景: Lightsail プラン選定で当初 $5 (nano dual stack) を希望したが、AWS CLI で get-bundles した結果、$5/dual stack は 0.5GB RAM であり、v1 が 461MB で破綻していたのと同程度のメモリ余裕しかないことが判明。$5 で 1GB を取るには IPv6 only プランしかなく、モバイルアプリ(IPv4 必須)には不適。

決定: micro_3_0 (1GB RAM / 2 vCPU / 40GB SSD / 2TB transfer / dual stack, $7/月) を採用。

理由:

  • v1 のメモリ問題(mod_php5 prefork で 392MB / 461MB 占有)の悲劇を避けたい
  • v2 の uvicorn + FastAPI は起動時 100-150MB と軽いが、ピーク時の余裕とバッファキャッシュ用に 1GB は欲しい
  • $5 → $7 の差額 $2/月は EC2 nano × 2 + ALB の現状($30/月以上)からは劇的なコスト減のうち

2026-04-27: Lightsail OS は Ubuntu 24.04 LTS、AZ は ap-northeast-1a

背景: Lightsail で利用可能な Linux ディストリビューションのうち、Ubuntu 24.04 / 22.04 / Amazon Linux 2023 などから選ぶ必要があった。

決定: Ubuntu 24.04 LTS、AZ は ap-northeast-1a。

理由:

  • Ubuntu 24.04 は LTS(〜2034 サポート)で長期運用に向く
  • Python 3.13 は OS 標準にはないが、uv python install 3.13 で即取得できるため OS 選定にほぼ影響なし
  • 情報量が多く、トラブルシューティングが容易
  • AZ は RDS footballnext と同じ ap-northeast-1a に揃え、ネットワークレイテンシを最小化

2026-04-27: Lightsail ↔ RDS の接続は VPC Peering + RDS SG に Lightsail CIDR /16 許可

背景: Lightsail インスタンスは Lightsail 専用 VPC に置かれ、AWS の default VPC(RDS が居る)とは別ネットワーク。接続経路として (a) RDS を public access に変更 + IP 制限、(b) VPC Peering 有効化 + RDS SG にプライベート IP 許可、の2案を比較。

決定: Lightsail VPC Peering を有効化(無料、API ボタン1つ)+ RDS の Security Group に inbound 172.26.0.0/16:3306 を追加(Lightsail 全体の private CIDR を許可)。

理由:

  • VPC Peering なら通信が AWS 内部完結、インターネット経由なし → セキュリティ・レイテンシともに有利
  • RDS の public access 化は影響範囲が広く、運用ミスで世界に晒すリスクあり、却下
  • /32 でインスタンスごとの IP を許可する案もあったが、再起動や再作成で IP 変動するたび手作業が増えるため、個人開発の運用コスト優先で /16 採用

2026-04-27: 開発時の S3 認証は AWS CLI プロファイル thomson-ik 経由

背景: v2 の S3 連携 (uploadRanking* バッチ) を実装するにあたり、開発時の AWS 認証情報をどう持たせるか3案あった: (A) 既存 ~/.aws/credentials の thomson-ik プロファイル流用、(B) .env に IAM アクセスキー埋め込み、(C) 専用の最小権限 IAM ユーザー新規作成。

決定: (A) の既存プロファイル流用を採用。aioboto3 は SDK のデフォルト認証チェーン(環境変数 → ~/.aws/credentials → IAM Role)を辿るので、コード側で profile 名を埋めず aioboto3.Session() だけで動く。起動時に AWS_PROFILE=thomson-ik を渡す運用。

理由:

  • 開発はローカルマシンに閉じる、追加の IAM 操作不要で即着手できる
  • コードに認証情報を埋め込まないので、本番デプロイ時もコード変更ゼロで動く(環境変数 / .env の値だけ切り替え)
  • 本番用の最小権限 IAM ユーザー作成 (s3:PutObject on schulte-thomsons のみ) は Lightsail 構築時に別タスクとして実施する方針。設計が綺麗

2026-04-27: schulte-server-v2 として Python / FastAPI で書き直す(v1 のレガシー継承を断念)

背景: schulte-server (v1) は Amazon Linux 1 + PHP 5.3 + CakePHP 2.0.4 + Apache 2.2 という EOL 続きのレガシースタックで、Lightsail 移行に際して PHP-FPM 化や OS/PHP アップグレードを行うには大幅な改修が必要。一方で実 API は 8 本のみ、認証なし、外部連携は S3 のみと構造が極めてシンプルだった。

決定: v1 のレガシースタック継承(PHP-FPM 化、PHP/OS アップグレード)を断念し、Python 3.13 + FastAPI で schulte-server-v2 として完全に書き直す。v1 と v2 を並行運用しつつ徐々に v2 に切り替える。

理由:

  • 実 API 8本という小規模で、書き直し工数は週末ハッカソン〜1〜2週間規模
  • レガシー継承は短期的にメモリ問題は解決するが、Lightsail 移行時に結局 PHP/OS アップグレードが必要になり「やり直し」感が強い
  • Python / FastAPI は LLM 補助との相性が最も良く、個人開発の長期保守に向く
  • DB は既存 RDS の schulte データベースを流用、書き換えコストは API コードのみで済む

2026-04-27: schulte-server-v2 リポジトリは operation 直下に物理配置、別 GitHub 管理

背景: v2 リポジトリの配置として、(A) ~/cdev/schulte-server-v2/ に独立配置、(B) ~/cdev/schulte-operation/schulte-server-v2/ に operation 直下同居、を比較した。

決定: operation 直下 ~/cdev/schulte-operation/schulte-server-v2/ に物理配置する。git 管理は別リポジトリ ichirokisanuki/schulte-server-v2 (private) として独立。operation 側の .gitignore に /schulte-server-v2/ を追加して除外する。

理由:

  • 既存の schulte-server/ schulte-unity/ と同じパターンで一貫性がある
  • v1 のソースと並べて参照しながら v2 を書ける(実装時の生産性が高い)
  • git 管理は完全に独立しているので互いの履歴は混じらない

2026-04-27: schulte-server-v2 の開発メモは operation 側 .devnotes/ に集約

背景: v2 リポジトリにも .devnotes/ を置くと「どっちに書くか問題」が発生し、メモが分散して .devnotes/ 運用の設計が壊れる懸念があった。

決定: v2 リポジトリには .devnotes/ を置かない。schulte-operation 全体を「アプリ運用全体のオペレーション」と位置付け、v2 開発のメモも operation 側の .devnotes/ 4ファイルに集約する。v2 リポジトリの README で「開発メモは operation/.devnotes/ 参照」と明記する。

理由:

  • 個人開発において運用と開発の境界は曖昧で、operation = アプリ運用全体のメモ置き場とする位置付けが綺麗
  • メモが一元管理されることで意思決定の追跡が容易
  • v2 リポジトリは「コードのみ、自己完結」のシンプル構造を保てる

2026-04-27: v1 の PHP-FPM 化(メモリ問題改善)は実施しない

背景: v1 のメモリ余裕がない原因 = mod_php5 prefork (各 child 約45MB × 9) を解決するため、Apache 2.4 + php-fpm 化(α: mod_fcgid + php-cgi、γ: httpd24 + mod_proxy_fcgi + php-fpm)の検証を進めていた。2号機を ALB から detach し、AppName タグ書き換えで Lambda 監視対象から外したところまで実施済み。

決定: PHP-FPM 化は実施しない。v2 路線への方針転換に伴い、v1 はメモリ問題を抱えたまま残り稼働期間を耐えさせる。

理由:

  • v1 を改修してもいずれ v2 に置き換えるため、改修コストが無駄になる
  • 本番は1台のみでも CPU 1%、ピーク 0.3 RPS と余裕があり、現状でも運用継続可能
  • 2号機は停止して、本番1台運用で v2 リプレース完了まで耐える運用に切り替える

2026-04-27: schulte-operation を「運用リポジトリ」と位置付け、実装ソースは同居させない

背景: リポジトリ直下に schulte-server/(PHP バックエンド)と schulte-unity/(Unity クライアント)を物理配置するが、これらをこの operation リポジトリ自身でバージョン管理するか、別リポジトリ管理のまま .gitignore で除外するかを決める必要があった。

決定: schulte-operation はインフラ・運用ドキュメント・ロードマップを置く「運用リポジトリ」と位置付ける。schulte-server/ schulte-unity/ は .gitignore で除外し、本リポジトリの履歴・コミット対象には含めない。

理由:

  • 実装ソースと運用情報をひとつのリポジトリに混ぜると、コミット履歴が肥大化し、運用変更と実装変更の区別が付きにくくなる
  • 特に schulte-unity はリポジトリサイズが大きそうで、operation 側に巻き込むのは現実的でない
  • 直近ロードマップで両者を CodeCommit → github.com に独立移行する予定があり、それぞれは独立したリポジトリのまま運用するのが自然

DEVLOG(作業ログ)

開発日誌

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


2026-10-13

18:33 - dev-timer #89 の vitals 再確認(1.2.4 で共有クラッシュの再発なし)と #63 の消し込み

やったこと:

  • dev-timer #89(Android 1.2.4 の共有クラッシュ再発なしを vitals で再確認)を通知前に実施。uv run scripts/play_vitals.py 9(読み取り専用)
    • vc9 の CRASH / ANR issue は直近 60 日(8/14〜10/14)で 0 件
    • NativeShare(NativeShareBroadcastListener.Initialize)は 30 件・5 ユーザーで、出たのは vc6〜vc7 のみ。9/29 時点の 35 件から減ったのは 60 日の窓が進んだため
    • vc9 の日次クラッシュ率は 9/28〜10/11 の 12 日分すべて 0.00%(1 日 60〜80 ユーザー)。10/7〜10/8 は行なし(データなしで 0 ではない)。freshness は DAILY が 10/12 分まで
    • ほかの issue は vc2 の SIGABRT 1 件と vc6〜vc7 の ANR 3 件だけで、どれも旧版
  • 9/29 の #85 は「新版の数日分では弱い証拠」だったが、2 週間分でも 0 件なので共有クラッシュの件は完了とした。#89 を done
  • dev-timer の pending を見たら、このプロジェクトの #63(9/13 期限「Android 版に iOS 1.2.3 の対応(結果画面ランキング表示)を適用する」)が残っていた。 schulte-unity で iOS1.2.3 が Android1.2.4 の祖先で、vc8 のコミット 239372734 が「結果画面の変更を同梱」なので対応済み。#63 も done。これで schulte-operation の dev-timer pending は 0 件

気づき:

  • dev-timer は done を手で付けないと pending のまま残るので、リリースで中身が済んだリマインダが期限切れで溜まる。他プロジェクトにも期限切れの pending が十数件ある(今日 9:00 の 7channel-operation #101 など)

次回やること:

  • Play Console のアラート 3 件と、journal の件数確認(どちらも前から残っている)

2026-09-29

20:22 - 2 週間後の再確認を登録し、vitals 取得スクリプトを scripts/ に追加

やったこと:

  • dev-timer #89 を登録(2 週間後の 10/13 20:21 に「Android 1.2.4 の共有クラッシュ再発なしを vitals で再確認」、project schulte-operation)。#85 の判定が「新版の数日分では弱い証拠」だったので、新版の利用者が増えたあとで確認し直す(You が 2 週間後を選択)
  • vitals 取得スクリプト scripts/play_vitals.py を追加(読み取り専用。uv run scripts/play_vitals.py [versionCode] [--days N] [--pkg <pkg>])。全バージョンの CRASH / ANR issue を常に出し、versionCode を渡すとその版の issue と日次クラッシュ率も出す。 実行して #85 のときの数字(NativeShare 35 件・5 ユーザー vc6→vc7、vc9 は issue 0 件、9/27 のクラッシュ率 0.00%・約 70 ユーザー)を再現できることを確認した

次回やること:

  • 10/13 の dev-timer #89 の通知で uv run scripts/play_vitals.py 9 を実行し、NativeShare の最終出現が vc7 のままか(vc9 に出ていないか)を見る

20:14 - dev-timer #85 の vitals 確認(1.2.4 で共有クラッシュは再発なし)

やったこと:

  • dev-timer #85(Android 1.2.4 の共有クラッシュ解消を vitals で確認)の中身を実施。Play Developer Reporting API を読み取りだけで叩いた
    • NativeShareBroadcastListener.Initialize のクラッシュ: 直近 60 日で 35 件・5 ユーザー、初出 vc6・最終出現 vc7。vc8 以降には出ていない
    • vc9(1.2.4): 9/20 以降、クラッシュ・ANR とも issue 0 件。クラッシュ率は 9/27 分だけ行があり 0.00%(約 70 ユーザー)。エラー側のデータは 20 時の時点で 17 時ごろまで反映
    • 判定は再発なし。ただし旧版でも 60 日で 5 ユーザーしか踏まないバグなので、vc9 の数日分で 0 件は強い証拠ではない(反する材料はなく、実機確認は済み)
  • dev-timer #85 を done にした(npx tsx bin/dev-timer.ts done 85)

決めたこと:

  • 他 3 アプリ(勉強タイマー / kowai-best / 断食タイマー)の NativeShare 修正は、各アプリの次回アップデートのときに直す(You 判断。DECISIONS.md にも記録)

気づき:

  • vitals API のつまずき(手順は WIP.md の「Android のビルド・配信手順」に書いた)
    • errorIssues:search の interval は、interval.startTime.year などのネストしたクエリ + timeZone.id=UTC でしか通らない。interval.startTime=<ISO 文字列> は 400、America/Los_Angeles も 400(Unsupported timezone)、省略すると空 {} が返る
    • クラッシュ率は crashRateMetricSet、件数は errorCountMetricSet(reportType が必須 dimension)。errorCountMetricSet に crashRate を渡すと 400
  • dev-timer の fired_at 列はサーバーのどこにも書かれない(通知は Google カレンダー側から出る)。発火時刻を過ぎても pending・fired_at null のままで、done は手動(done N)

次回やること:

  • WIP.md の「次にやること」を参照(Play Console のアラート 3 件、journal の件数)

2026-09-25

20:07 - vitals 確認のリマインダーを登録(セッションの締め)

やったこと:

  • dev-timer に「Android 1.2.4 の共有クラッシュ解消を vitals で確認」を登録(#85、2026/9/29 20:06 に通知、project schulte-operation)

気づき:

  • dev-timer の疎通確認は /api/health。/health は nginx で 404 になる(スキルの手順書は /health のまま)

次回やること:

  • 9/29 の通知で vitals を確認(NativeShareBroadcastListener.Initialize が 1.2.4 = vc9 で出ていないこと)。 あわせて、同じ不具合が残る他 3 アプリ(勉強タイマー / kowai-best / 断食タイマー。各 .devnotes に記録済み)の修正に着手するか判断

19:55 - 1.2.4 (vc9) の公開を確認し、タグ Android1.2.4 を付与

やったこと:

  • 公開の確認: Play Developer API の製品版トラックは vc9「1.2.4 (9)」のみ。ストアページもバージョン 1.2.4(最終更新日 2026/09/24)
  • schulte-unity bf0b855(vc9 をビルドしたツリー。HEAD と一致を確認)に軽量タグ Android1.2.4 を打ち、CodeCommit に push(AWS_PROFILE=thomson-ik)
  • 公開 3〜4 日後の vitals 確認(NativeShare のクラッシュが 1.2.4 で消えたか)を dev-timer に登録

19:31 - Play の 16KB 警告の真因は共有ボタンのクラッシュ(NativeShare)。修正版 1.2.4 (vc9) を製品版に申請

やったこと:(作業は 2026-09-24〜25)

  1. Play Console の「リリースが 16 KB ページサイズに対応していない可能性があります」を切り分け
    • 1.2.4 (vc8) の製品版申請時に、ラボの 16KB 端末でクラッシュ検出として審査送信がブロックされた。表示されたクラッシュ内容は SystemUptimeMs: 352613 だけで手がかりにならない
    • AAB 内の arm64 .so 17 個を ELF で検査: PT_LOAD の p_align はすべて 0x4000、16KB ページを共有する LOAD なし、RELRO の保護範囲が他セグメントにかからない → すべて OK
    • パッケージング: Unity 2022.3.62f2(AGP 7.4.2)は launcher が useLegacyPackaging true、BundleConfig の uncompressNativeLibraries は無効、manifest は extractNativeLibs=true → zip アラインメントは無関係
    • 16KB エミュレータ kowai16k(android-35 ps16k、PAGE_SIZE=16384)に bundletool で入れると、起動・広告表示まで正常。monkey(シード 777)で再現: 設定の「お友達にアプリを広める」で IllegalArgumentException: Targeting S+ ... requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent(NativeShareBroadcastListener.Initialize)
  2. 原因: Assets/Plugins/NativeShare/Android/NativeShare.aar(2021-05 ビルド)の Initialize は SDK_INT >= 31 の分岐が空で、FLAG_UPDATE_CURRENT しか渡していない。 targetSdk 31+ では Android 12+ で共有を開いた瞬間に必ず落ちる。16KB とは無関係で、ラボのロボットが 16KB 端末で共有ボタンを押しただけ
    • Android vitals(Play Developer Reporting API、6/1〜9/24)でもクラッシュ 1 位(36 件 / 6 ユーザー、製品版 vc7)。公開中の 1.2.3 でも起きていた
  3. 修正: 上流 v1.5.5 と同じく SDK_INT >= 31 で FLAG_MUTABLE(1 << 25)を付けたクラスを、Unity 同梱 JDK の javac(--release 8、android-36 の android.jar)でコンパイルし、 AAR 内 classes.jar の同名クラスだけ差し替えた。ソースは patches/nativeshare-flag-mutable/。AndroidBundleVersionCode 8 → 9(versionName は 1.2.4 のまま)
    • 16KB エミュで、修正前は共有ボタンで 100% クラッシュ、修正後は共有シートが開く。monkey 7,000 イベント × 2(シード 777 / 4242)でクラッシュ・ANR ゼロ
  4. 署名ビルド: Claude デスクトップアプリの Bash からは ~/Dropbox(実体は ~/Library/CloudStorage/Dropbox)の keystore が macOS の権限で読めず、 :launcher:signReleaseBundle が Operation not permitted で失敗(サンドボックスを外しても同じ)。You がアプリ内ターミナルタブで CIBuild を実行して成功(既存の成果物を再利用して 36 秒)
    • 検証: versionCode 9 / 署名証明書の SHA-256 が vc7・vc8 と一致 / 16KB OK / リリース dex に 0x0a000000(FLAG_UPDATE_CURRENT | FLAG_MUTABLE)/ symbols.zip の Build ID が AAB の libil2cpp・libmain・libunity と一致(ネイティブは vc8 と同一なので symbols.zip の日付は 9/13 のまま)/ mapping の pg_map_id が dex と一致
  5. 配信: scripts/play_upload_internal.py で vc9 を内部テストへ(mapping・symbols 添付、リリース名「1.2.4 (9)」)→ You が実機で共有と結果画面を確認し、製品版 1.2.4 (9) を審査に送信
  6. コミット: schulte-unity bf0b855 を CodeCommit に push(AWS_PROFILE=thomson-ik が必要。default プロファイルは無い)。運用リポに patches/(9d834d7)と scripts/play_upload_internal.py(27c717b)
  7. 横展開: ~/cdev の NativeShare.aar を検査すると、勉強タイマー・kowai-best・断食タイマーが同じ未修正 AAR(バイト単位で同一)。 vitals でもそれぞれクラッシュ 1 位(101 件 / 79 人、101 件 / 53 人、2 件 / 2 人)。vision-training と gabor は修正済みの版。3 プロジェクトの .devnotes(ROADMAP / WIP)に修正予定を記録して push

決めたこと:

  • 16KB 警告は「このまま続行」で押し切らず、原因を直した vc9 で申請し直す(DECISIONS)
  • NativeShare は問題のクラス 1 つだけを差し替えるパッチで直し、プラグイン全体の更新は見送る(DECISIONS)
  • 他 3 アプリの修正は後日。各プロジェクトの .devnotes に記録(You 判断)

気づき:

  • Play の 16KB 警告の「ラボでクラッシュ検出」は 16KB 由来とは限らない。ラボの 16KB 端末で起きたクラッシュは何でもこの見出しで出る。 まず .so(PT_LOAD・RELRO・ページ共有)とパッケージングを確認し、問題が無ければ 16KB エミュで monkey を回して実際のスタックトレースを取る
  • 表示されるクラッシュ内容(SystemUptimeMs: …)はクラッシュ記録のヘッダ行で意味がない。vitals のクラッシュ上位を先に見れば早かった
  • monkey は物理キーの無い端末だと --pct-syskeys 0 にしないと即中断する。adb root すると adbd が再起動してバックグラウンドの logcat が切れる
  • macOS の keytool は Java が入っていないとスタブ。Unity 同梱 JDK(PlaybackEngines/AndroidPlayer/OpenJDK/bin/keytool)を使う

次回やること:

  • 審査結果の確認 → 公開後にタグ Android1.2.4、vitals で NativeShare のクラッシュが 1.2.4 で出ていないか確認

2026-09-11

19:14 - トムソンの指示で閾値未満の既存行に del=1 を付与。閾値直上に残る偽造 ID は BAN しない判断

やったこと:

  1. 閾値の決め方をトムソン向けに説明(LINE 用のプレーンテキストにして You が送付)。 手順は「0.5 秒刻みのヒストグラム → 正規の立ち上がりの手前にある山を偽造クラスタとして切り分け → タップ速度で妥当性確認」
  2. トムソンから「異常値でランクインしている行に del を立てて」と指示 → 実施
    • 閾値未満の del=0 行に del=1, updated=NOW() を付与: 4x4 914 行 / 5x5 1,800 行 / 6x6 5,886 行 / 7x7 1,582 行(計 10,182 行)
    • 戻せるよう対象の id / player_id / score を Lightsail の ~/del-bogus-20260911/modeN.tsv に保存してから更新
    • S3 のランキング 20 ファイル(daily / weekly / total の TSV、daily / weekly の JSON)を upload エンドポイントで再生成
  3. 再生成後の全期間トップが各モードとも閾値ちょうど上(2.0 / 4.00016 / 7.0003 / 15.0007)だった
    • 今日の反応ではなく 2023〜2025 年の古い行。送信者は del=1 にしたのと同じ偽造 ID(CcLqG1W、BnbcqfG、dH2VImR、oj0uk8e、THIJuta)で、 閾値のすぐ上に数百行ずつ持っている(例: CcLqG1W は 6x6 の 7.0〜7.7 秒に 939 行)
    • 8/30 に整理した「閾値を置くとすぐ上に残る」がそのまま出た形。行単位ではもう切れない
    • is_ban=1 の候補 5 人(+4x4 のみの 4 人)を提示したが、You の判断で「閾値に収まっているなら BAN しない」。 それ以上はセッショントークン方式(次回アプデ以降)に委ねる

気づき:

  • 偽造値は「整数 + 0.0001〜0.001」の形(4.00016、7.0003、15.0007)で並ぶ。ツールが整数秒を狙って出している痕跡で、 閾値を整数に置くとその直上に固まって見える。閾値の効果を報告するときは「トップがどう見えるか」まで確認してから伝える
  • del=1 の一括更新は「対象 id を先にファイルへ書き出す → id IN で更新 → del=0 の残数ゼロを確認」の順にすると、 他の del=1 行と混ざらずに戻せる

18:42 - スコアの異常値(モード別下限)を本番投入し、24 時間ランキングをイベント駆動の即時更新に切り替え

やったこと:

  1. トムソンの依頼「各モードの異常値を求めて sendScore で弾く」に対応
    • 本番 RDS の全期間データ(4x4 158 万行 / 5x5 189 万行 / 6x6 29 万行 / 7x7 31 万行)を Lightsail 上のスクリプト(アプリの DB 接続設定を流用)で集計。分布・低スコア帯のヒストグラム・占有プレイヤーを確認
    • 「前回送信からの経過秒 < 申告タイム」で偽造者を切り分ける案は不採用。timestamp はサーバー受信時刻なので、 オフラインで溜めた分を一括送信する正規ユーザーが引っかかり、間隔を空けて送る偽造者(4x4 を 0.35 秒で 14 回)は素通りする
    • 閾値は分布のギャップとタップ速度で決めた。4x4 2 秒 / 5x5 4 秒 / 6x6 7 秒 / 7x7 15 秒 未満を異常値 (You の原案は 6x6 5 秒 / 7x7 12 秒。6x6 は 5.0〜6.5 秒に 1 人が約 4700 行の山、7x7 は 12〜15 秒に山があり、 どちらも正規の立ち上がり(6x6 8 秒台、7x7 16 秒台)より下なので引き上げた)
    • schulte-server-v2 8c7ffac: MIN_VALID_SCORE 未満は保存せず isSuccessed: true と playerId を返す (偽造者に閾値を悟らせない)。playerId 空のときのプレイヤー発行は従来どおり。弾いた件は sendScore rejected bogus score: mode=… score=… player=… で journal に残る
    • 本番で「閾値のすぐ下(1.9 / 3.9 / 6.9 / 14.9)は未保存」「閾値ちょうどは保存」を確認。 テスト行 4 件と自動発行の空プレイヤー 8 件は削除済み。閾値はトムソンに共有済み
  2. 24 時間ランキングのイベント駆動即時更新(2026-09-09 の試算どおり実装)
    • schulte-server-v2 f4b0a67: sendScore で保存後、そのプレイヤーの 24 時間内ベストを更新していた場合だけ 当該モードの daily(TSV + JSON)を asyncio.create_task で再生成して S3 に置く。応答は待たせない
    • 同一モードの再生成中に来た要求は「保留」にまとめ、終了後 1 回だけ再実行(asyncio.Lock + pending セット)。 状態はワーカープロセスごと(uvicorn workers=2)なので最悪 2 回作り直すが害はない
    • upload エンドポイントの本体を _upload_ranking_tsv / _upload_ranking_json に分離して共用
    • 本番で 3 パターン確認: 新規プレイヤー → 3 秒後に S3 に反映 / 遅いスコア → 再生成なし / 速いスコア → 再生成。 1 回 0.2 秒前後。テスト行 3 件とプレイヤー 1 件は削除し、daily を再アップロードして S3 からも消去
    • Lightsail の crontab で daily を 5 分ごと → 30 分ごとの保険に変更(バックアップ ~/crontab.bak-20260911)。weekly / total は据え置き

気づき:

  • Lightsail(1GB RAM)で 190 万行を Python に載せると OOM で黙って死ぬ。重い並べ替えは MySQL のウィンドウ関数に寄せ、 Python 側には集約結果と低スコア帯だけ持ってくる。長い集計は nohup でファイルに書かせ、ssh は結果取得だけにする
  • 52.68.176.128 への ssh は最初の接続が時々タイムアウトする。-o ConnectionAttempts=3 で再試行すれば通る
  • 「閾値未満は不可能」の線を引くとき、マスが増えるほど 1 タップあたりの探索時間が伸びる (正規最速帯の速度は 4x4 4.9 回/秒、5x5 4.0 回/秒、7x7 2.8 回/秒)ので、モードごとに速度換算で見直すと 6x6・7x7 は緩すぎに気づける
  • 本番のテストは v2-test- プレフィックス運用を踏襲。ランキングに載る側のテストは巨大な遅いスコア(9999 秒)で末尾に置き、 削除後に upload エンドポイントを叩いて S3 から消す

次回やること:

  • 閾値未満の既存行(合計約 1 万行)を del=1 にするかは保留(You 判断)。やればランキングから即消える
  • 本命のセッショントークン方式は引き続き次回アプデ以降

16:57 - セッションまとめ。TestFlight 1.2.3 (1) の処理完了を確認

やったこと:

  • App Store Connect API で iOS 1.2.3 ビルド 1 が processingState=VALID になったことを確認(2026-09-09 04:36 PDT アップロード分)。 内部グループ「aaa」に自動配布されるので TestFlight で試せる状態
  • .devnotes の WIP を書き直し(アーカイブ化)、ROADMAP にランキング即時更新の検討項目を追加

次回やること:

  • TestFlight で確認 → schulte-unity を CodeCommit にコミット・タグ iOS1.2.3 → App Store 審査提出(コミットは You の判断待ち)

2026-09-09

21:40 - ランキング即時更新の負荷試算と、結果画面モックのトムソン向け共有

やったこと:

  1. 「ランクインの度に 24 時間ランキングを即時更新したら負荷は?」を本番 DB の直近 7 日で試算
    • スコア送信: 1,963 件/日(最大 268 件/時)
    • 24 時間ベスト更新(=ランキングが変わるイベント): 610 件/日、送信の 31%。最大 68 件/時、p95 45、中央値 25。 混むのは 13 時・19 時・21 時ごろ(JST)
    • 24 時間一覧は各モード 56〜193 人しか居ないので、24 時間ベスト更新はすべてランクイン
    • 1 回の作り直し(daily の TSV + JSON)は 0.15〜0.3 秒。イベント駆動にしてもピークで CPU 稼働率 0.6%、 S3 PUT は 1,220 回/日で、現行 cron の daily 分(2,304 回/日)より少ない。送信のたびに更新しても 1,963 回/日
    • 実装案: sendScore で 24 時間ベスト更新時だけバックグラウンドで当該モードの daily を再生成し、cron は 30 分の保険に落とす。 アプリ更新は不要。未実装、ROADMAP に積んだ
  2. 6 状態モックに各状態の説明文を追加し、トムソン向けに S3 へ公開
    • thomson-share スキル(visionworkout-operation / study-timer-ops にある)の規約に従い、 thomsons バケットにキー 周辺視野トレ_結果画面6状態モック_20260909.html で public-read。HTTP 200 確認済み
    • アーティファクト用 HTML は <title> 直書き形式なので、単体で開けるよう doctype と head/body で包み直した
    • 同じモックの Claude アーティファクト: https://claude.ai/code/artifact/61571980-4fe7-4cb0-8b6c-b565a0dcb0ed

気づき:

  • 24 時間ランキングは「参加人数が少ない(数十〜200 人)」ので、ランキングが変わるイベント数 = ベスト更新数で見積もれる
  • thomson-share スキルはこのプロジェクトには未配置。必要ならコピーする

2026-09-09

20:36 - 結果画面の見た目を反復して確定 → 実機検証 → iOS 1.2.3 (1) を TestFlight にアップロード

やったこと:

  1. 実機(iPhone 14)での確認環境を整備
    • Assets/Editor/CIBuild.cs に BuildIOS() を追加。Unity バッチ書き出し → xcodebuild → xcrun devicectl device install app をコマンドだけで回せるようにした(1 サイクル約 5 分)
    • 署名は Thomson Inc.(JFBRCYHN24)。RAMANU LLC(9HZW92277N)で試すと 「Bundle ID は別チーム所有」で弾かれた。ストア版と同じチームなら上書きインストールしてもデータは引き継がれる
    • devicectl install はロック中でも通るが、process launch はロック解除が必要
  2. 結果画面の設計を反復(アーティファクトのモックで 6 状態を並べて確認しながら進めた)
    • 3 軸(全期間ベスト / 24 時間ベスト / 上位 500)で 6 状態に整理。全期間ベスト更新 ⇒ 24 時間ベスト更新なので 8 通りにはならない
    • 「あなたの記録」カード案 → 順位が今回の記録に付いたものではないのに大きく出る矛盾があり撤回
    • 「ランキング常時表示 + 説明 1 行」→ 文言(届かず / おめでとう / ランクインならず)と大きさを何度か変えた末、説明行は全廃
    • 最終仕様: ランキングは常に表示。自分の行は 24 時間ベストの 1 行だけ。今回がベストとして載ったら赤(#FA4F49)、 今回は届かず今日のベストが載っているだけなら薄い赤(#E8938F)。圏外は自分の行なしで一覧の末尾 7 行
    • 実装は RankingRow.Setup(..., isMeDimmed) と ResultWithRankingAdditiveScene.SetupRankingArea (窓の中心を is_this_time ではなくプレイヤー ID で決めるようにした)。LocalDB.MergeScoreThisTime はそのまま
    • 一時的に作った SetupRecord / SetupStatus は削除済み
  3. TestFlight へアップロード
    • PlayerSettings.iOS.buildNumber を 1 に(バージョン 1.2.3)。1.2.2 は「1.2.2」「2」の 2 本だった
    • xcodebuild archive → -exportArchive(method app-store-connect / destination upload)で直接アップロード。 Upload succeeded(20:34)。Thomson Inc. の Distribution 証明書はローカルに無かったが -allowProvisioningUpdates で通った
    • ITSAppUsesNonExemptEncryption = false 済みなので輸出コンプライアンスの確認は不要
    • 内部グループ「aaa」は全ビルド自動配布なので、処理が終わればそのまま TestFlight に並ぶ

気づき:

  • 見た目の判断は実機スクショ 1 枚で一気に進む。今回は You が実機で目視、モックはアーティファクトで反復した
  • iPhone ミラーリングは Claude 側から起動できず、接続には You の操作が要る
  • schulte-unity の変更は まだ CodeCommit にコミットしていない(コミットとタグ iOS1.2.3 は You の判断待ち)

18:12 - 結果画面を「ベスト更新ならランキング、それ以外は自分の記録」に変更(クライアント、未ビルド)

やったこと:

  1. 設計の見直し: 当初は「今回がベストでないときも今回行を差し込む」案だったが、You の 「自分の記録と全体の記録の 2 軸にしては」という提案を受け、条件切り替えに変更
    • 24 時間ランキング内の自己ベスト更新時(初プレイ含む)→ 従来どおり自分の行を中心にランキング 7 行
    • 更新しなかったとき → 順位は変わらないので「あなたの記録」(24 時間ベストと順位 / ひとつ上の順位までの差 / ベスト更新までの差)を同じ枠に出す。「もっと見る」で全体ランキングにはいつでも行ける
    • タブ案は初期表示の判断が結局同じになる点とシーン編集が要る点で不採用
    • 切り替え判定の「自己ベスト」は 24 時間一覧内のベスト(NEW RECORD 表示は従来どおり全期間ベスト)
  2. 実装(schulte-unity、3 ファイル、シーン編集なし)
    • LocalDB.InsertScoreThisTime → MergeScoreThisTime(mode, pid, score, out myBest) に置き換え。 自分の行を 24 時間ベスト 1 行だけに保ち、今回がベストなら is_this_time=1 で入れて true、 届かなければ既存ベストを is_this_time=0 で残して false。SQL はパラメータバインドに変更 (従来の {score} 文字列埋め込みはカンマ小数点ロケールで SQL が壊れていたはず)
    • RankingRow.SetupRecord(orderLabel, label, time?, isMe) を追加(行プレハブを記録行として流用)
    • ResultWithRankingAdditiveScene.SetupRankingArea を分岐。見出しも「24時間ランキング」/「あなたの記録」 (英語は DAILY RANKING / YOUR RECORD)に切り替え。圏外(上位 500 人外)は「圏外」+「N位まであと」
    • 併せて予定していた SendScore の score.ToString(CultureInfo.InvariantCulture) 修正を同梱
  3. 検証
    • 端末 DB テンプレート(StreamingAssets/schulte.db)のコピーに同じ SQL を流し 6 ケース確認 (ベスト未更新 / さらに悪化 / 更新 / タイ / 自分の行なし / 空一覧)
    • Unity 2022.3.62f2 バッチモードでコンパイル成功(エラー 0、警告は既存分のみ、15 秒)
    • 実行時の見た目は未確認(Editor の Play で結果画面を出すのが最短)。ビルド・コミットも未実施

気づき:

  • 結果画面の上部にはすでに「今回の記録」「自己ベスト(全期間)」が出ているので、「あなたの記録」枠は 24 時間ベスト・順位・差分だけに絞った
  • schulte-unity は CodeCommit 管理(master)。変更は作業ツリーに残したまま

2026-09-08

15:55 - ランキングをプレイヤーごとの自己ベストのみに変更(サーバーのみ、デプロイ済み)

やったこと:

  1. 現状把握: 24時間ランキングに同じ人が何行も並ぶ件の規模感を調査
    • クライアントは API ではなく S3 の静的ファイル(rankings/{daily,weekly,total}/modeN.{tsv,json})を 読んでいる。生成元は v2 の _fetch_ranking_rows() 1 関数のみ
    • 実測(S3 の各ファイルのユニーク人数): 24時間 4x4 = 897 行中 105 人 / 週間 5x5 = 1000 行中 27 人 / 全期間 6x6 = 1000 行中 5 人(1 人で 989 行)
    • クライアントは受け取った行をそのまま描画しているだけなので、サーバー修正だけで全画面に効く
  2. _fetch_ranking_rows() を GROUP BY player_id + MIN(score) の派生表に書き換え (schulte-server-v2 commit 05ec7b8)
    • 併せて players.is_ban = 1 を除外(BAN 済みは 4 人。v1 時代からずっとランキングで未参照だった)
  3. 本番 RDS にインデックス追加: idx_del_player_score (del, player_id, score) を modeN_scores 4 テーブルに ALGORITHM=INPLACE, LOCK=NONE で追加(mode6 2.9s / mode7 3.6s / mode4 11.0s / mode5 21.0s)
    • 追加前: 全期間 4x4 の集計が 16.4 秒。追加後: 約 1 秒(Using index)
    • 24時間・週間は既存の idx_del_ts_score で 0.04 秒なので元々問題なし
    • クエリ形 4 案(派生表 JOIN / 内側 LIMIT / GROUP BY 単体 / 相関サブクエリ)を比較したが 全て約 1 秒で差が無く、厳密で単純な派生表 JOIN 案を採用
    • schema/init.sql にもインデックス定義を反映
  4. デプロイ → 生成エンドポイント 20 本を手動実行 → S3 の全 12 TSV で「行数 = ユニーク人数」を確認
    • 生成時間: 24時間・週間 0.12〜0.16 秒、全期間 0.3〜1.4 秒(30 分ごと × 4 モード)
    • 変更後の行数: 24時間 105 / 193 / 59 / 56、週間 412 / 647 / 179 / 190、全期間は各 1000(4x4→7x7 の順)

気づき:

  • 結果画面の「今回のスコア」行は端末側で挿入している(LocalDB.InsertScoreThisTime)ため、 S3 側で重複を消しても「自分の過去ベスト + 今回」の 2 行は残る。1 行にするならクライアント修正(次回アプデ)
  • 全期間の GROUP BY は MySQL 8.4 でも loose index scan にはならず、カバリングインデックスの全走査 + 一時表になる。 それでも 1 秒台なので追い込まない
  • Lightsail 上で PYTHONPATH=~/apps/schulte-server-v2 ~/.local/bin/uv run python script.py とすれば、 mysql クライアント無しでアプリの接続設定を使って本番 DB に EXPLAIN / DDL を流せる

2026-08-30

17:56 - スコア偽造対策の設計整理と、7channel 証明書の横展開確認

やったこと:

  1. スコア偽造対策を検討(次回アプデ向け・実装はしていない)

    当初「下限チェック + is_ban + 既存データ掃除」を提案したが、対症療法だという指摘を受けて設計を見直した。

    • 下限を設けても偽造者は閾値のすぐ上を出すだけ。しかもその値は人間にも可能な範囲なので 検出不能な方向に追いやることになり、かえって悪化する
    • is_ban も手動発見が前提。playerId を空で送れば新規プレイヤーが自動発行されるので付け替え放題
    • 根本は「サーバーがプレイされた事実を一切持っていない」こと。 ゲーム開始を知らせるエンドポイントが無く、unique_key もクライアント生成 (DateTime.Now + ランダム7文字)で偽造可能。二重送信防止の役割しかない

    実測した偽造データ(各モードの最小値とタップ速度 / 中央値):

    モード マス 中央値 最小値 タップ速度
    4x4 16 7.08秒 0.35秒 46回/秒
    5x5 25 15.4秒 0.92秒 27回/秒
    6x6 36 30.5秒 1.99秒 18回/秒
    7x7 49 60.7秒 5.50秒 8.9回/秒

    6x6 は 2.70 と 4.59 の間に明確なギャップがあり、偽造クラスタが分離して見える。

    対策の段階(1〜4)と移行策は WIP.md に記録。要点は:

    • 本命は段階2(セッショントークン)。サーバーが実経過時間を知るので 申告値 < 実経過時間 を嘘と判定でき、物理的にありえない時間が本当に不可能になる
    • 移行の肝は「証明付きのスコアだけランキングに載せる」。旧クライアントは送信も 自己ベストも従来どおりで、ランキング掲載のみ対象外。移行期間を切る必要がない
    • 戦略的代替案としてランキングの位置づけ自体を下げる手もある(実装コストほぼゼロ)

    is_ban はスキーマにあるが _fetch_ranking_rows() で参照されていない(del_ == 0 のみ)ことも判明。

  2. 7channel の証明書を横展開で確認 → 問題なし

    schulte の失効原因(certbot が dns-route53 で AWS 権限不足のまま失敗し続けた)が 他サーバーにもないか確認した。

    • 7channel は Lightsail micro 35.73.116.227(多目的サーバー 13.230.63.19 とは別)
    • authenticator は最初から webroot(HTTP-01)。AWS 認証情報に依存していない
    • 証明書は 2026-08-03 発行 / 11-01 期限 = 4週間前に自動更新が成功している
    • certbot renew --dry-run も成功
    • 監視の cert:443 も入っているので、失敗しても残30日(10月上旬)で通知が飛ぶ

気づき:

  • 「とりあえず弾く」対策は、検出可能な不正を検出不能な不正に変えることがある。 閾値を置く前に「回避されたらどうなるか」を考える必要がある
  • 障害の原因を特定したら、同じ構成が他にないか横展開で確認するのが定石。 確認方法は /etc/letsencrypt/renewal/*.conf の authenticator を見て certbot renew --dry-run を通すだけで済む

2026-08-29

16:50 - 422 の正体はスコア偽造ではなくロケール由来のバグ。サーバー側で止血

背景: 未決事項に挙げていた「422 の継続発生」と「スコア偽造」が関係しているかを調査した。

やったこと:

  1. 404 と 422 を切り分け

    • 404 は /.env /.git/config /wp-content/plugins/... など汎用スキャナのノイズ。無関係
    • 422 は POST /v1/sendScore のみ、24時間で59件。他のパスは一切叩いていない=狙い撃ちに見えた
  2. 発生元 IP を突き合わせ → 重複ゼロ

    • 422 を出した IP: 9個 / 59件
    • 200 を出した IP: 326個 / 2023件
    • 両方に登場する IP は 0個。422 組は一度も成功していない
  3. nginx ログの User-Agent で正体が判明

    • 422 の発生元はすべて正規のアプリ(UnityPlayer/2022.3.62f2 ... / SchulteTable/2 CFNetwork/...)
    • curl や Postman のような手作りリクエストではなかった=攻撃ではなく実ユーザーの送信失敗
  4. 地域の偏りから原因を特定

    • 発生元はメキシコ・ベトナム・スペイン・ブルガリア・オランダ・フランス — いずれも小数点にカンマを使うロケール。日本国内からの 422 はゼロ
    • 本番で検証: score=25.74 → 200 / score=25,74 → 422
    • 原因はクライアントの form.AddField("score", "" + score)。 "" + float がカルチャ依存の文字列化になり、カンマ圏では 25,74 を送っていた
  5. サーバー側で受け止める修正をデプロイ — schulte-server-v2 commit 3ff3a20

    • score を str で受け、parse_score() で寛容にパース
    • カンマのみ → 小数点として解釈 / 桁区切りと混在 → 流儀を判別できないので拒否 / NaN・Inf も拒否
    • 単体テスト13ケース通過。本番デプロイ後、既存 unique_key を使った非破壊テストで 25,74 0,5 188,03 がパース成功することを確認(既存キーなら挿入せず早期 return する性質を利用)

気づき:

  • 「攻撃っぽいログ」を攻撃と決めつけない。 User-Agent と地域分布を見るまで、 当初は「スキャナか不正リクエスト」と記録していた。実際は自社ユーザーが困っていた
  • 影響は 24時間で約 2.8% の送信失敗。該当地域のユーザーは毎回失敗していたはずで、 体感は「スコアが記録されない」。8/28 に You が遭遇した症状が一部の国では常態化していた
  • サーバー側で受け止めると既存の全バージョンが即座に救われる。 クライアント修正だけだと旧バージョンのユーザーは次回更新まで救えない

次回やること:

  • クライアント側の根本対応(次回リリースに含める): form.AddField("score", "" + score) → score.ToString(CultureInfo.InvariantCulture) 他の数値フィールドにも同じ書き方が無いか併せて確認する

13:43 - 実機・Android 16 検証 → 製品版へ昇格。Android に課金導線が無いことが判明

やったこと:

(1) 実機でスコア送信の復旧を確認(Honor 20 / Android 10)

  • adb でゲームを自動プレイ(グリッドをスクショから読み取り 1〜16 を順にタップ)し、結果画面まで到達
  • 08:43:12 POST /v1/sendScore 200 OK(送信元 153.218.124.180)をサーバーログで確認
  • 24時間ランキングにも反映(370位)。「スコアデータの送信に失敗しました」は再現せず
  • 実ユーザーの送信も復旧しており、直近1時間で 200 が198件

(2) 内部テスト版 (vc7 / 1.2.3 / targetSdk 36) を実機で検証

  • 起動・プレイ・結果画面・スコア送信・ランキング取得すべて正常
  • 08:54:20 POST /v1/sendScore 200 OK で回帰なしを確認

(3) Android 16 (API 36) の挙動をエミュレータ (fn_api36) で検証

  • bundletool で AAB からデバイス別 APK を生成してインストール
  • edge-to-edge 強制: UI の見切れなし。上部・下部ボタンともシステムバーに隠れない
  • 大画面での向き固定: wm density 240 で sw720dp を再現し横向きに回転 → アプリは縦長レターボックスで表示され、横向きに引き伸ばされなかった
    • Android 16 の互換モード UI("See and do more" / "Double-tap to move this app")が出るのが正常な挙動
    • 追加した android:appCategory="game" が期待どおり機能している
  • エミュレータの density / 回転設定は元に戻した

(4) Android には課金導線が存在しないことが判明

  • 一時停止画面の「広告を非表示」ボタンが出ない → 原因を調査
  • [HomeScene.cs] #if UNITY_IOS で PurchasingManager.Initialize() が iOS 限定
  • [SettingAdditiveScene.cs] UpdateProButtonVisible() は #if UNITY_ANDROID で無条件に非表示
  • 実機ログでも Billing 関連の出力は 0行
  • 履歴を追った結果:
    • IAP は 6fd49dd25(2025-03-05) で導入、当時 vc=2
    • 9875028cc(2026-01-05, vc=4)「リリースした」で #if UNITY_IOS を追加
    • つまりコード上は10か月ほど Android でも初期化を試みる状態だった
  • ただし Play Console に Android 用の商品が無ければ初期化は NoProductsAvailable で失敗するため、 実際に Android で購入できた時期があったとは言えない。You の認識(Android には一度も実装していない)が正しい
    • Play Console の商品有無は API から確認できず(旧 inappproducts は 403「新 API に移行してください」、 新 monetization/* は 404)。画面で確認が必要
  • 今回の Billing 8 更新で Android の課金が壊れる余地は無い(そもそも動いていない)。 Play の要件は同梱ライブラリのバージョンで判定されるので条件は満たしている

(5) ネイティブデバッグシンボルの作成

  • Publishing Settings の「Create symbols.zip」が無効で symbols.zip が生成されていなかった
  • Library/Bee/artifacts/ に未ストリップ版が残っていたので、build-id を AAB と照合して組み直した
    • objcopy_5lmi/libil2cpp.sym.so / Android/libunity/arm64-v8a/unstripped/libunity.so / AsyncPluginsFromLinker/arm64-v8a/lib_burst_generated.so / Unity 同梱 Variations/il2cpp/Development/Libs/arm64-v8a/libmain.so
    • 4ファイルとも build-id 一致・.symtab あり
  • 成果物: Builds/schulte-1.2.3-vc7-native-debug-symbols.zip(21MB)
  • libil2cpp.dbg.so は 378MB で Play の上限超過のため、シンボルテーブル版を採用(行番号は出ない)

(6) 製品版へ昇格

  • 昇格は You が Play Console で実施。当初 production status=draft vc=7 で止まっていたのを指摘 → 審査提出
  • 最終状態: production status=completed vc=7(vc6 は置き換え済み)

詰まったこと / 気づき:

  • ネイティブデバッグシンボルは API で後付けできない apks/{vc}/deobfuscationFiles/nativeCode に投げると 400 FAILED_PRECONDITION: The App Bundle with version code 7 is added previously - the Deobfuscation File can't be modified. → バンドルをアップロードしたのと同じ編集セッション内でしか添付できない。 先日 proguard マッピングが通ったのは同じ編集内だったため。後付けは Play Console の画面から行う
  • Library/ を消す前で助かった。消していたら vc7 用のシンボルは復元不能だった
  • インタースティシャル広告(AppLovin 経由の TikTok プレイアブル)が 3分以上ループして閉じるボタンが出ず、 戻るキーも効かなかった。UI ツリーにも閉じる要素が無い(WebView 内)。 結果画面に到達できずその回のスコアが失われた。2回目は頻度上限で広告が出ず正常だった
  • サーバーに 422 が継続的に発生(直近1時間で 200:198件 / 422:40件 / 404:25件)。 発生元は 57.131.28.6 5.117.164.174 23.236.199.202 など複数の海外 IP に分散
  • ランキング上位に文字化けユーザー名で 0:01:95〜0:02:22 という不自然な記録が並んでいる。 既知課題の「認証機構なし=スコア偽造可能」がそのまま出ている
  • zsh の落とし穴: "$h:ProjectSettings/..." は :P を修飾子(絶対パス化)として解釈される。 git のリビジョン指定を組み立てるときは bash を使うか引用を分ける

次回やること:

  • ネイティブデバッグシンボルを Play Console の画面からアップロード
  • 審査通過後にアラート3件が消えたか確認
  • SNS の購読に普段使いのアドレスを追加
  • 広告が閉じない件と 422 の調査(優先度は要判断)

2026-08-28

18:50 - Android 16 / Billing 8 対応で内部テスト配信 + SSL 失効による33日間の障害を復旧

やったこと:

(1) Google Play の 8/31 期限アラート3件への対応(Android)

  • アラートの内容を切り分け:
    • ① アップデート公開には targetSdk 36 が必要
    • ② 新規ユーザー配信には製品版トラックが API 35 以上である必要がある
    • ③ Google Play Billing Library 8 以上が必要
  • 現状は targetSdk 34 / com.unity.purchasing 4.12.2(Billing 6.2.1)
  • com.unity.purchasing は 5.x ではなく 4.15.1 を選択
    • 4.15.1 は Billing 8.3.0 を同梱しつつ 4.x の旧 API のままなので、 PurchasingManager.cs(IStoreListener / UnityPurchasing.Initialize)を書き換えずに済む
    • 5.x は API 総取っ替えで、3日で終わる規模ではない
  • 段階的にビルド検証(すべてエラー0):
    • ベースライン(API 34 / IAP 4.12.2): 5分31秒
    • API 36 のみ: 3分07秒
    • API 36 + IAP 4.15.1: 4分31秒
  • Unity 2022.3.62f2 のままで API 36 は通った。エディタ更新は不要だった
    • 同梱 SDK に android-36 があり、AndroidApiLevel36 も enum に存在
    • 懸念していた aapt2 の RES_TABLE_TYPE_TYPE エラーは出なかった (Minify Release が元から ON なのが効いている)
    • build-tools は 34.0.0 のままで compileSdk 36 が通る
  • 実ビルドで判明した要対応: android:appCategory="game" がマージ後 manifest に出ていない
    • AndroidIsGame: 1 は Android TV バナー用途で、Unity 2022.3 は appCategory を出力しない
    • このままだと Android 16 の大画面(sw600dp+)で screenOrientation="userPortrait" が 無視され、タブレットで回転・リサイズ可能になる
    • Assets/Plugins/Android/AndroidManifest.xml に appCategory="game" と PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY を追加して解消
  • 16KB ページサイズのアラインメント警告はゼロ(sqlite-android-3390400.aar の arm64 .so も 0x4000 整列を実測確認)
  • versionCode 6 → 7 / bundleVersion 1.2.2 → 1.2.3

(2) 署名済み AAB を作って内部テストへ配信

  • keystore パスワードは一時ファイル(~/.schulte-keystore-pass)経由で受け渡し、 ビルド後にランダムデータで上書きして削除
  • ビルドスクリプトは ProjectSettings.asset にパスワードが永続化されないよう finally で必ず空に戻す作りにした(git diff で混入なしを確認済み)
  • 署名検証: keystore の証明書と AAB 署名者証明書の SHA-256 が完全一致
  • Play Developer API(play-console-mcp@ichirokisanuki.iam.gserviceaccount.com)で アップロード → マッピングファイル添付 → 内部テストトラック割り当て → commit
    • マッピングの添付先は AAB でも apks/{versionCode}/deobfuscationFiles/proguard
    • commit だけ 403 になり、サービスアカウントに「テストトラックへのリリース」権限を 付けてもらってから成功
  • 結果: internal = versionCode 7 (1.2.3) / production = 6 (1.2.2) のまま

(3) 【障害】スコア送信が33日間壊れていた — 実機テスト中にユーザーが発見

  • 症状: 「スコアデータの送信に失敗しました」が iOS / Android 両方で発生
  • 原因: Let's Encrypt 証明書が 2026-07-26 に失効していた(ssl_verify=10)
  • なぜ自動更新が失敗していたか:
    • certbot の authenticator が dns-route53 で、Route53 の DNS-01 チャレンジを使う設定
    • certbot は root で動くため Lightsail のインスタンスロールにフォールバックするが、 AmazonLightsailInstanceRole に Route53 権限がない
    • .env の IAM ユーザー schulte-server-v2-s3 も S3 専用で、しかも別 AWS アカウント
    • 4/27 の初回取得は別の認証情報で通したため、その場では気づけない構成だった
    • certbot.timer 自体は毎日動いていた(=「動いているのに失敗し続ける」状態)
  • 対処: 認証方式を dns-route53 → webroot(HTTP-01)に変更
    • nginx の 80番ブロックに /.well-known/acme-challenge/ の location を追加
    • 両ドメインの証明書を再発行(2026-11-26 まで有効)
    • AWS 認証情報への依存が消えたので、同じ壊れ方はしなくなった
  • 検証: HTTPS 5回連続 200 / ssl_verify=0 / 34ms、certbot renew --dry-run 両ドメイン成功、 POST /v1/sendScore の疎通確認(空データで早期 return、本番への書き込みなし)
  • サーバ自体は終始正常だった(メモリ 543/911MB、負荷 0.00、アプリはリクエストを処理し続けていた)

(4) 監視の穴を塞いだ(senrigan-observer 側で対応、commit 1e4c277)

  • 監視設定自体は正しかった(HTTPS + 証明書検証あり)。鳴らなかった理由は2つ:
    • 通知先が goda.takeshichan2@gmail.com と ichirokisanuki.apple@gmail.com だけで、 普段使いの ichirokisanuki@gmail.com が入っていない
    • detect_transitions() が状態遷移時の1通しか出さない設計 → 見落とすと永久に沈黙
  • 対処: 証明書の残日数チェック(残30日で警告)と、NG 継続中の24時間ごと再通知を実装

詰まったこと / 気づき:

  • Unity のバッチビルドは同一プロジェクトで多重起動できない。前のビルドの Unity プロセスが残っている状態で次を投げて exit 134 になった
  • 別セッションで fasting-unity の Android ビルドが並行稼働しており、 CPU と Gradle デーモンを取り合ってビルドが遅くなっていた
  • CloudWatch Logs は IAM ユーザー Ichiro に logs:FilterLogEvents が無く読めなかった。 7/26 に実際にメールが飛んだかの直接確認はできていない(コード挙動からの推定)
  • 「監視が動いていること」と「異常に気づけること」は別物だった。今回は Lambda も certbot.timer も毎日正常に動いていたが、どちらも結果を誰にも届けていなかった

次回やること:

  • SNS の購読に普段使いのアドレスを追加(AWS コンソールでの承認が必要)
  • 実機で購入フロー(定期購読 / 買い切り / 復元)と Android 16 の UI を確認
  • 製品版への昇格、または 8/31 までに Play Console から 11/1 への期限延長申請

2026-04-28

12:25 - 撤退完了 + v2 リソース増強 + RDS Private 化で月$24削減 + phpmyadmin 引き継ぎ書

やったこと:

(1) v2 のリソース増強(深夜帯の安定運用に向けて)

  • 起動から長時間稼働での安定性を分析
  • swap 1GB 追加 (/swapfile、fstab で永続化) → OOM 時のセーフティネット
  • uvicorn workers=2 に変更 (--workers 2) → 長時間処理でブロックしない
  • メモリ available 356MB + swap 1GB、systemd Restart=on-failure で完成形
  • 追加コストゼロ(既存 Lightsail プランのリソース内)

(2) 撤退作業 残タスク完遂(観測時間を待たずに進めた)

  • DNS の alb-v1 (Weight 0) レコード削除(切り戻し用残置を解除)
  • ALB taki から schulte 用 listener rule × 2 (HTTP/HTTPS) を削除
  • ALB schulte TG 自体を削除(taki ALB 自体は他4アプリ用に残置)
  • Lambda 3つ (TCM_observeALB_*) の Schulte 行をコメントアウトして再デプロイ
  • EC2 1号機・2号機を terminate-instances で完全削除
  • dev-timer #4 (4/29 19:50 terminate リマインダ) を done でキャンセル

(3) EIP 課金問題の深堀り

  • ユーザー指摘で「EC2 stopped 後の Public IPv4 課金は $0 (auto-assigned IP は解放済み)」を確認
  • AWS account 内の EIP 一覧を確認 → 5 個中 4 個が InstanceId: null、月 $14.6 の浪費に見えた
  • 詳細確認したところ: 4個とも実は RequesterManaged=true で AWS 管理(ALB taki 用 × 2 / RDS 用 × 2)
  • ALB 用 2 個は ALB を残す限り解放不可(amazon-elb 管理)

(4) RDS の PubliclyAccessible=false 化(コスト削減 + セキュリティ向上)

  • baseballnext footballnext の PubliclyAccessible を true → false に変更(即時反映)
  • 副作用: amazon-rds が管理してた EIP 2 個 (3.115.155.164 / 52.198.72.205) が自動解放 された
  • 月 -$7.3 のコスト削減 + DNS 解析で RDS が見えなくなる security 向上

(5) phpmyadmin 接続不可問題の発覚と方針決定

  • (4) の副作用で、別 account の Lightsail (mydb.ikapps.com の phpmyadmin) から footballnext / baseballnext へ接続不可に
  • 「Public 経由で繋いでた」前提が崩れた
  • ユーザー判断: Public 戻し(月 $7.3 増)は不採用、SSH トンネル方式で復活させる方針
  • 作業は別リポジトリ (~/cdev/common-php-my-admin) 側で行うため、引き継ぎ.md を作成 → 即作業開始できるよう「背景・構成案・やること詳細・必要情報・注意点」を網羅

(6) kizuna RDS の調査(PubliclyAccessible=true なのに EIP がない件)

  • 別 account (ichirokisanuki, 776364707178) の RDS、2015年作成
  • Public IP は auto-assigned(describe-addresses には出ないが、2024-02 以降 月 $3.65 課金されている)
  • 前回の説明「PubliclyAccessible=true → EIP が付く」は 不正確だったと訂正
  • 正しい説明: 「Public IP(auto-assigned が標準、EIP は特殊ケース)が付く。いずれも 2024-02 以降は課金対象」
  • footballnext / baseballnext は EIP が手動 associate されてた特殊状態だったことが判明
  • kizuna も同じ仕組みで Private 化すれば月 $3.65 削減可能(オプション、phpmyadmin SSH トンネル整備後に検討)

(7) ロードマップ整理

  • 今月の撤退タスクすべて完了マーク
  • 残るのは「明朝 dev-timer #3 通知での観測(自動)」と「今四半期: schulte-unity GitHub 移行」のみ

決めたこと:(→ DECISIONS.md 参照)

  • v2 のリソース増強: swap 1GB 追加 + uvicorn workers=2
  • 本番 RDS は PubliclyAccessible=false に統一する方針
  • phpmyadmin 経由のアクセスは SSH トンネル方式で代替(Public に戻さない)
  • ALB 用 EIP (amazon-elb RequesterManaged) は触らない(ALB を維持する限り release 不可)

詰まったこと / 気づき:

  • pydantic-settings の extra='forbid' のときに journalctl にエラー値が露出した過去のヒヤリハットを思い出す形で、AWS のエラーメッセージや IAM operation も同様に「変数値が出る可能性」を意識
  • OperationNotPermitted は権限ではなくリソース管理状態(RequesterManaged)が原因
  • modify-db-instance で --apply-immediately --no-publicly-accessible は数十秒で反映、ENI の Public IP が外れて EIP も自動解放(amazon-rds 管理だから)
  • 2024-02 から auto-assigned Public IPv4 にも課金されるようになった点は、見落としやすい

次回やること:

  • 明朝の dev-timer #3 (2026/4/28 7:42) はもう過ぎているので、結局 wrap up 前にスナップショット観測を兼ねた
  • 共通 phpmyadmin 側で SSH トンネル化(別リポジトリ ~/cdev/common-php-my-admin/引き継ぎ.md 参照)
  • その後オプションで kizuna も PubliclyAccessible=false 化(月 -$3.65 追加削減)
  • 大物として schulte-unity の CodeCommit → GitHub 移行(今四半期内)

2026-04-27

19:43 - 撤退作業の最初の一歩: 1号機停止 + 12h タイマー

やったこと:

(1) dev-timer に翌朝の確認用リマインダ登録

  • --in 12h --project schulte-operation "v2 切り替え後の安定稼働確認 (翌朝)"
  • ID #3、発火予定 2026/4/28 7:42:16(明朝)

(2) 1号機 (i-039d46fb13dc9f81d) の停止

  • AppName タグ: Schulte → Schulte_maintenance(2号機と同じ運用、Lambda 自動再起動防止)
  • ALB schulte TG から deregister-targets (draining 開始)
  • stop-instances 実行(running → stopping → 数十秒で stopped へ)
  • 順序が大事: AppName 変更を先にしないと、stop 後に startEC2 Lambda が拾って再起動してしまう

詰まったこと / 気づき:

  • detach Lambda (TCM_observeALB_detachEC2FromTargetGroup) は AppName でフィルタリングしてないので、unhealthy 7分後に「detach + stop」+ SNS アラートを鳴らす可能性があった。手動 deregister + stop で先回りして回避

次回やること:

  • 翌朝 (12h タイマーで通知が来てから) v2 が深夜帯〜朝のピークを耐えたか確認
  • 問題なければ撤退の続き: DNS alb-v1 レコード削除 → ALB schulte リスナールール削除 → ALB schulte TG 削除 → Lambda 整理 → EC2 terminate

19:33 - 🎯 本番 DNS 切り替え完了 (10% → 50% → 100%)、v2 が完全稼働

やったこと:

(1) 本番切り替え発射 (10% Lightsail / 90% ALB)

  • change-resource-record-sets で schulte.thomsonsapp.com を Weighted Routing 構成に変更
    • DELETE 旧 Alias レコード → CREATE alb-v1 (Weight 90, Alias) + lightsail-v2 (Weight 10, A=52.68.176.128)
  • INSYNC 確認、dig 50回で実測振り分け確認
  • 10 分観測: cron インスタンス (52.194.87.168) からの uploadRanking* 3件が v2 で 200 OK
    • 一番重要な動作: 本番 S3 バケット schulte-thomsons への書き込みが v2 経由で成功
  • Unity からの sendScore はサンプル小(0.3 RPS × 10% × 10分 ≒ 数件期待)でログには現れず

(2) 50% に拡大

  • alb-v1 / lightsail-v2 ともに Weight 50 に UPSERT
  • 10 分観測:
    • Unity SchulteTable/2 CFNetwork/... からの POST /v1/sendScore が 27 件全部 200 OK(実 iOS クライアント動作確認)
    • cron uploadRanking* も 19 件全 200 OK
    • 5xx エラーゼロ、uvicorn 例外ゼロ、メモリ余裕

(3) /v1/healthcheck 404 問題

  • ユーザーから「移管されてない」報告 → ブラウザで https://schulte.thomsonsapp.com/v1/healthcheck を叩いたら 404
  • 原因究明: /v1/healthcheck は v1 のみ実装 (datetime 文字列を返す)、v2 は /healthcheck のみ実装
  • ログを見ると senrigan-observer/0.1 という外部監視サービスが /v1/healthcheck を死活監視していた → 50% 振り分けの半分が 404 でアラート出る恐れ
  • 対応案: (a) v2 に /v1/healthcheck 互換実装 / (b) 100% v2 後に監視先を /healthcheck に変更
  • ユーザー判断で (b) を採用、ただし先に 100% v2 に切り替える順序で

(4) 100% v2 に切り替え

  • alb-v1 → Weight 0 (削除はせず残置、即時切り戻し用)
  • lightsail-v2 → Weight 100
  • INSYNC 確認、dig 50回 → 50/50 全部 Lightsail に向く
  • ユーザー側で監視サービスの URL を /v1/healthcheck → /healthcheck に変更(404 アラート解消)

(5) 100% 切り替え後 5 分観測

  • ALB schulte TG への RequestCountPerTarget: 19:26 以降 0 RPM (切り替え時刻から完全にゼロ)
  • v2 へのトラフィック: 5分で 24 リクエスト = avg 0.08 RPS
  • エンドポイント別: sendScore 33 / uploadRankingTsvToS3 23 / uploadRankingScoresAndPlayersJsonData 23 / sync 4 / healthcheck 9
  • 200 ステータス: 79 件、5xx ゼロ、uvicorn 例外ゼロ、メモリ available 424MB

決めたこと:(→ DECISIONS.md 参照)

  • 監視サービス (senrigan-observer) の URL を /v1/healthcheck → /healthcheck (v2 仕様) に変更
  • ALB schulte TG への振り分け (alb-v1 レコード) は Weight 0 で残置、即時切り戻し用に保持

詰まったこと / 気づき:

  • DNS Weighted Routing は確率分散なので、サンプル数 50 では誤差 ±10〜20% は普通。実トラフィックが少ないと観測値が想定からズレる
  • 大切なのは「ALB 経由のリクエスト数 (CloudWatch メトリクス)」を見ること。直接的に「v2 にどれくらい流れたか」がわかる
  • /v1/healthcheck の存在を v2 で実装し忘れていたが、結果的に「100% 切り替え後に監視を v2 標準の /healthcheck に揃える」が綺麗な解決になった

次回やること:

  • 数時間〜1日の安定運用観測
  • 観測 OK 後、撤退フェーズ:
    • ALB schulte TG から 1号機 (i-039d46fb13dc9f81d) を deregister
    • 1号機 EC2 停止
    • DNS の alb-v1 (Weight 0) レコード削除
    • Lambda TCM_observeALB_* の Schulte 関連分削除
    • ALB taki の schulte リスナールール削除 (taki ALB は他4アプリで継続使用)

18:51 - 本番ドメイン切り替え準備完了(証明書、Nginx、切り戻し情報)

やったこと:

(1) schulte.thomsonsapp.com の Hosted Zone と現在の DNS レコード把握

  • 委任先 zone id: Z035498717N5DORY02AM0(thomsonsapp.com 本体 zone から NS で分離されてる)
  • 現在の A レコード: Alias で dualstack.taki-879563088.ap-northeast-1.elb.amazonaws.com. (ALB taki)
  • HostedZoneId of ALB: Z14GRHDCWA56QT、EvaluateTargetHealth: true

(2) certbot 用 IAM ユーザー schulte-server-v2-cert のポリシー更新

  • route53:ChangeResourceRecordSets の Resource を 2 zone に拡張
    • arn:aws:route53:::hostedzone/ZN306RXECIMLN(thomsonsapp.com)
    • arn:aws:route53:::hostedzone/Z035498717N5DORY02AM0(schulte.thomsonsapp.com)

(3) schulte.thomsonsapp.com 用 SSL 証明書取得(DNS-01 challenge)

  • Lightsail 内で certbot certonly --dns-route53 -d schulte.thomsonsapp.com
  • 取得成功、有効期限 2026-07-26、自動更新タスクあり

(4) Nginx config 更新

  • server_name schulte.thomsonsapp.com の HTTPS server block を追加
  • v2-test.thomsonsapp.com 用も並存(default_server も兼ねる)
  • HTTP 80 は default_server _ で全ホスト名を受け(cron からの HTTP curl 用)

(5) 動作確認(本番 DNS 未変更、curl --resolve で Lightsail に直接接続)

  • curl --resolve schulte.thomsonsapp.com:443:52.68.176.128 https://schulte.thomsonsapp.com/...
  • 証明書 CN = schulte.thomsonsapp.com、Issuer = Let's Encrypt → 正しい証明書が返る
  • /v1/sync で本番データ取得 OK、v2-test 経路も並存して動作 OK

(6) 切り戻し情報の inline 記録(バックアップファイルは作らずここに記録)

切り戻し用情報(重要):

現在の schulte.thomsonsapp.com (zone Z035498717N5DORY02AM0) の A レコードは Route53 Alias:

{
    "Name": "schulte.thomsonsapp.com.",
    "Type": "A",
    "AliasTarget": {
        "HostedZoneId": "Z14GRHDCWA56QT",
        "DNSName": "dualstack.taki-879563088.ap-northeast-1.elb.amazonaws.com.",
        "EvaluateTargetHealth": true
    }
}

→ 切り戻し時はこの内容で change-resource-record-sets の UPSERT を実行する。

切り替え時の v2 用 A レコード:

{
    "Name": "schulte.thomsonsapp.com.",
    "Type": "A",
    "TTL": 60,
    "ResourceRecords": [{"Value": "52.68.176.128"}]
}

詰まったこと / 気づき:

  • schulte.thomsonsapp.com は別 zone に NS 委任されてる構成だった。同じ thomsonsapp.com zone 内にあると思い込んでた
  • Alias レコードは TTL を直接持たず Route53 が短い TTL で管理する → TTL 短縮の追加作業は不要。Lightsail 用に切り替えると Alias から普通の A レコード (TTL 60) に変わる
  • .devnotes/ 配下に backup ディレクトリを作ったが、session-wrap-up スキルの「4ファイル以外を作らない」ルールに違反するので削除し、切り戻し情報は本 DEVLOG にインライン記録する方針に切り替え

次回やること:

  • 本番切り替え発射: Route53 Weighted Routing で v2 を 10% → 50% → 100% に段階切り替え
  • 各段階で systemd journal を観測(500 エラー / レスポンスタイム異常)
  • 100% 到達後、v1 EC2 2台停止 + 関連 Lambda (TCM_observeALB_*) 整理

18:07 - HTTPS 化完了、v2 が https://v2-test.thomsonsapp.com で全エンドポイント動作

やったこと:

(1) Lightsail ファイアウォール 443 開放(22/80/443 の3 port)

(2) Route53 IAM 権限の追加

  • ichirokisanuki ユーザーは attached managed policy が 10 個(上限)
  • 11個目を attach できないため、インラインポリシー Route53Access (route53:*) で対応
  • 既存の LightsailFullAccess インラインに続いて2件目

(3) Unity クライアント URL 確認

  • schulte-unity/Assets/ を grep → 全エンドポイント https://schulte.thomsonsapp.com/v1/... で叩いてる
  • ALB taki にも HTTPS 443 リスナーがあり、ACM 証明書 (arn:aws:acm:.../4eec...) でサーブ中
  • Lightsail に切り替えるなら独自に SSL 証明書が必要(ACM は ALB/CloudFront 専用、Lightsail には流用不可)

(4) DNS 切り替え方針の検討

  • ALB ターゲットは VPC 内 IP のみ → Lightsail インスタンスは ALB 配下に入れられない
  • → DNS 切り替え方式に確定
  • 切り替え時のリスクを下げるため v2-test.thomsonsapp.com サブドメインで事前検証 案を採用

(5) v2-test.thomsonsapp.com 用 DNS レコード作成

  • thomsonsapp.com の Hosted Zone ID ZN306RXECIMLN
  • A レコード: v2-test.thomsonsapp.com → 52.68.176.128、TTL 60秒
  • INSYNC 確認 + Google DNS で浸透確認

(6) certbot 用 IAM ユーザー作成 (最小権限)

  • schulte-server-v2-cert 新規作成
  • インラインポリシー CertbotDnsRoute53:
    • route53:ListHostedZones, route53:GetChange (Resource: *)
    • route53:ChangeResourceRecordSets (Resource: arn:aws:route53:::hostedzone/ZN306RXECIMLN 限定)
  • アクセスキー発行 → Lightsail /etc/letsencrypt/aws-credentials (mode 600) に配置

(7) Let's Encrypt 証明書取得 (DNS-01 challenge)

  • apt install certbot python3-certbot-dns-route53 (certbot 2.9.0)
  • certbot certonly --dns-route53 -d v2-test.thomsonsapp.com --email ichirokisanuki@gmail.com --agree-tos
  • 取得成功、有効期限 2026-07-26、自動更新タスクもセットアップ済み
  • DNS-01 challenge を選んだ理由: 本番 DNS 切り替えに依存せずいつでも証明書取得・更新できる

(8) Nginx SSL 設定

  • 80 / 443 両方で受ける構成(80 は cron からの HTTP curl 用)
  • listen 443 ssl http2 default_server(nginx 1.24 では旧形式 listen ... ssl http2; が必要、新形式 http2 on; は未対応)
  • TLSv1.2 + TLSv1.3、HIGH:!aNULL:!MD5

(9) https://v2-test.thomsonsapp.com で全 8 エンドポイント動作確認

  • /healthcheck, /healthcheck/db, /v1/sync, /v1/uploadRankingTsvToS3, /v1/uploadRankingScoresAndPlayersJsonData すべて 200
  • 本番 RDS / 本番 S3 への書き込みも確認

決めたこと:(→ DECISIONS.md 参照)

  • v1 → v2 切り替えは v2-test.thomsonsapp.com で事前検証 → 本番ドメインは Route53 Weighted Routing で段階切り替え
  • 証明書取得は DNS-01 challenge(certbot-dns-route53 plugin)で行う

詰まったこと / 気づき:

  • IAM の attached managed policy には ユーザーあたり 10 個の hard limit がある。インラインポリシーは別枠なので、上限を超えそうなら Inline で。
  • Ubuntu 24.04 の nginx 1.24 では http2 on; 新ディレクティブ非対応。listen ... ssl http2; の旧形式を使う
  • IAM ユーザーのアクセスキー発行直後は数十秒の propagation 遅延があるのが恒例。AccessDenied や InvalidAccessKeyId が出ても 30 秒待てばたいてい通る
  • certbot は systemd timer で自動更新タスクを勝手にセットしてくれる(Ubuntu apt 経由インストール時)

次回やること:

  • schulte.thomsonsapp.com 用の SSL 証明書も DNS-01 で先に取得(切り替え時の手間ゼロ)
  • 本番 schulte.thomsonsapp.com の TTL を短縮(現在いくつかは要確認)
  • Route53 Weighted Routing で v2 を 10% → 50% → 100% に段階切り替え
  • 観測 → 問題なければ完了、問題あれば即 ALB に戻す
  • v1 EC2 2台の停止 + 関連 Lambda (TCM_observeALB_*) の整理

17:51 - Lightsail で systemd 化 + Nginx + 専用 IAM、v2 が本番相当環境で完全動作

やったこと:

(1) systemd service 化

  • /etc/systemd/system/schulte-server-v2.service を作成
  • User=ubuntu, WorkingDirectory=~/apps/schulte-server-v2, ExecStart=uv run uvicorn ...
  • Restart=on-failure, RestartSec=5 で自動再起動
  • enable + start → active running, memory 76.8MB(軽量)

(2) Nginx インストール + リバースプロキシ

  • nginx 1.24.0 を apt 経由で導入
  • /etc/nginx/sites-available/schulte-server-v2 を作成
    • listen 80 default_server
    • proxy_pass http://127.0.0.1:8000 + 標準的な X-Forwarded-* ヘッダ
  • default サイトを無効化、新 config を enable → reload
  • 外部から http://52.68.176.128/healthcheck で 200 確認

(3) 専用 IAM ユーザー作成(最小権限)

  • schulte-server-v2-s3 ユーザーを新規作成
  • インラインポリシー S3PutObjectRankings:
    • s3:PutObject + s3:PutObjectAcl on arn:aws:s3:::schulte-thomsons/rankings/* のみ
    • DELETE 不可、他バケット不可、それ以外も不可
  • アクセスキー発行 → Lightsail .env に追加(DB_* と並べて)
  • systemd の EnvironmentFile=/home/ubuntu/apps/schulte-server-v2/.env で Python プロセスに環境変数として渡す

(4) ヒヤリハット: AWS_SECRET_ACCESS_KEY が journalctl に露出

  • pydantic-settings v2 のデフォルトが extra='forbid' で、.env に Settings 未定義の AWS_* を書いた瞬間バリデーションエラー
  • エラーメッセージ input_value='...' に secret access key の生値 が含まれた
  • それが journalctl -u schulte-server-v2 の出力に乗り、ローカルターミナル / Claude session context に残った
  • 対処:
    • 漏洩キー AKIA4S3DSMNCJ6IVYWKR を発覚から約60秒以内に delete-access-key で無効化(実害ゼロ)
    • app/config.py に extra='ignore' を明示してコミット b6fedd2
    • 新キー発行 → Lightsail .env を再生成 → 今度はエラーなく起動
  • 教訓: pydantic のエラー出力は変数値を含む。秘密情報を扱う設定では必ず extra='ignore' を明示すること

(5) 全 8 エンドポイント本番動作確認

  • /healthcheck, /healthcheck/db(MySQL 8.4.8)
  • /v1/sync 37 players / 500 scores
  • /v1/uploadRankingTsvToS3/4/1/0 → S3 に書き込み (810 rows / 19,637 bytes)
  • /v1/uploadRankingScoresAndPlayersJsonData/4/1/0 → S3 に書き込み (79 players / 500 scores / 22,465 bytes)
  • S3 で LastModified が更新されていることを aws s3api list-objects-v2 で確認

決めたこと:(→ DECISIONS.md 参照)

  • v2 用 IAM ユーザーは schulte-server-v2-s3、ポリシーは s3:PutObject + s3:PutObjectAcl on rankings/* の最小権限
  • pydantic-settings の Settings には必ず extra='ignore' を明示する(秘密情報露出の再発防止)

詰まったこと / 気づき:

  • IAM アクセスキー発行直後の propagation 遅延で InvalidAccessKeyId が出た(30秒待ったら通った)。前の AttachStaticIp の遅延と同パターン。IAM 系は数十秒見ておくのが安全
  • systemd の EnvironmentFile= は KEY=VALUE 形式の .env をそのまま読める。pydantic-settings の env_file と同じファイルを共用できて便利
  • journalctl の出力に変数値が乗ることへの認識が薄かった。Bash tool で取得する前にどんな出力か想定する習慣を持つべき

次回やること:

  • Lightsail ファイアウォール: 443 を開ける(80 はリダイレクト用に残す)、8000 は閉じたまま
  • Let's Encrypt で SSL 証明書取得(certbot + nginx プラグイン or DNS-01 challenge)
  • DNS の準備: 現在 schulte.thomsonsapp.com は ALB 経由。Lightsail 静的 IP に切り替える手順整備(TTL 短縮 → 切り替え → 戻し)
  • 切り戻し手順の整備(v2 で問題が出たら ALB 経由に戻す)

17:32 - Lightsail インスタンス構築 + v2 が Lightsail 上で動作確認

やったこと:

(1) IAM 権限の準備

  • ichirokisanuki ユーザーに Lightsail 操作権限がなかった
  • インラインポリシー LightsailFullAccess (lightsail:*) を新規追加
  • Lightsail 専用の AWS マネージドポリシーは存在しないので、最小限のインライン定義
  • IAM 反映に約 30 秒かかったが、その後は問題なし

(2) Lightsail プラン・OS 選定

  • 当初 $5 (nano dual stack) を希望したが、AWS CLI で get-bundles した結果 nano = 0.5GB RAM だと判明
  • v1 (461MB) で破綻していた経緯から余裕を持たせて micro_3_0 ($7/月, 1GB RAM, dual stack) を採用
  • OS は Ubuntu 24.04 LTS(情報量・サポート期間の長さ)
  • AZ は ap-northeast-1a(RDS footballnext と同 AZ、ネットワークレイテンシ最小)

(3) Lightsail インスタンス作成

  • 名前: schulte-server-v2
  • 静的 IP: 52.68.176.128 (allocate + attach)
  • VPC Peering 有効化(無料、Lightsail コンソール 1 ボタン相当の API)
  • Lightsail のデフォルト鍵 (LightsailDefaultKey-ap-northeast-1) をローカルに download

(4) RDS Security Group 調整

  • RDS footballnext の SG に inbound rule を追加: TCP 3306 from 172.26.0.0/16 (Lightsail VPC)
  • /16 で広めに許可(個人開発のため運用簡略優先、再起動で IP 変わっても影響なし)

(5) Lightsail インスタンス内セットアップ

  • uv 公式インストーラーで導入(~/.local/bin/uv)
  • uv python install 3.13 で Python 3.13.13 取得
  • gh CLI を apt 経由で導入
  • ssh-keygen で ed25519 鍵を生成 → 公開鍵を GitHub に登録(ユーザー手動)
    • 自動登録を試みたが、ローカル gh トークンに admin:public_key スコープがなく失敗、手動登録に切り替え

(6) v2 デプロイと動作確認

  • git clone git@github.com:ichirokisanuki/schulte-server-v2.git ~/apps/schulte-server-v2
  • .env 配置(DB_HOST は VPC peering 経由で直接 RDS、ポート 3306 そのまま)
  • uv sync で依存インストール成功
  • uvicorn 起動 → /healthcheck 200, /healthcheck/db で MySQL 8.4.8 接続確認, /v1/sync?mode=4 で本番データ取得 (37 players / 500 scores)

決めたこと:(→ DECISIONS.md 参照)

  • Lightsail プラン: micro_3_0 (1GB RAM, dual stack, $7/月)
  • Lightsail OS: Ubuntu 24.04 LTS、AZ ap-northeast-1a
  • Lightsail ↔ RDS の接続: VPC Peering + RDS SG に 172.26.0.0/16:3306 を inbound 許可

詰まったこと / 気づき:

  • Lightsail の AttachStaticIp が一度 AccessDenied で失敗。原因は IAM ポリシー追加直後で propagation が遅延(30秒待ってリトライ成功)。policy simulator では allowed と判定されてた
  • gh ssh-key add で SSH 鍵を自動登録しようとしたが、ローカル gh のトークンに admin:public_key スコープがなく NG。gh auth refresh -s admin:public_key で追加できるが対話的なので手動登録を選択
  • Lightsail の最新料金: 同じ $5 でも dual stack だと 0.5GB、IPv6 only だと 1GB。モバイルアプリ用途では dual stack 必須なので $5/0.5GB or $7/1GB の二択
  • Lightsail インスタンスの bash heredoc は set -e 入れないと中途半端に止まることがある(ssh -T git@github.com は exit 1 で正常終了)

次回やること:

  • systemd service 化(uvicorn を常時稼働、再起動時に自動起動)
  • Nginx リバースプロキシ(80/443 → 8000)
  • 本番用最小権限 IAM ユーザー作成(s3:PutObject on schulte-thomsons のみ)+ アクセスキー → .env に追加
  • Lightsail ファイアウォールで 443 を開ける、8000 は閉じたまま
  • DNS 切り替え + Let's Encrypt SSL(v1 → v2 切り替えのタイミングで)

17:06 - v2 にバッチ系2本実装 + S3 連携、v1 同等機能の API 全実装完了

やったこと:

(1) S3 認証方針の決定

  • 開発時: ローカル ~/.aws/credentials の thomson-ik プロファイルを使用(aioboto3 が SDK デフォルトチェーンで自動取得)
  • 本番時 (Lightsail): IAM Role for EC2 相当の機能がないため、別途 IAM ユーザー作成 + アクセスキー → Lightsail 内 .env に配置運用を想定(次フェーズ)

(2) v1 のバッチ実装を再確認

  • getRankingTsvData(mode, term): term=1 daily / 2 weekly / 3 monthly / 4 total、score 昇順 1000件
  • getRankingScoresAndPlayersJsonData(mode, term): 同条件で 500件、{players, scores} 形式
  • 出力先: s3://schulte-thomsons/rankings/{daily|weekly|monthly|total}/mode{N}.{tsv|json}
  • ACL: public-read(クライアントが直接 S3 から取得)

(3) v2 実装

  • app/s3.py 新規: aioboto3 セッション + put_public_object() ヘルパー(バケット・リージョン定数化)
  • app/main.py に共通関数追加: _fetch_ranking_rows() _rows_to_tsv_bytes() _rows_to_json_payload()
  • GET /v1/uploadRankingTsvToS3/{mode}/{term}/{delay} 実装
    • TSV は csv.writer(delimiter='\t') で v1 PHP fputcsv 互換(タブ・改行・クォートを含む name のエスケープを v1 と揃える)
  • GET /v1/uploadRankingScoresAndPlayersJsonData/{mode}/{term}/{delay} 実装
    • JSON は json.dumps(separators=(',', ':'), ensure_ascii=False) で v1 PHP json_encode のスペースなし出力に合わせる
    • UTF-8 直出力なので v1 の \uXXXX エスケープよりサイズが小さい
  • delay パラメータは await asyncio.sleep(delay) でサポート(cron が同時実行を分散させる用途)

(4) 本番 S3 で動作確認

  • mode=4 term=1 (daily) で TSV/JSON 両方をアップロード成功
  • v1 cron 直前出力 → v2 上書き → アップロード前後で LastModified が更新されたことを確認
  • TSV: 796 rows / 19,372 bytes
  • JSON: 80 players / 500 scores / 22,478 bytes
  • 内容も日本語名・絵文字含めて正常
  • 5 分以内に v1 cron が次の周期で上書きするので影響残らず

(5) v2 commit & push

決めたこと:(→ DECISIONS.md 参照)

  • 開発時の S3 認証は ~/.aws/credentials の thomson-ik プロファイル経由(コードに認証情報を埋めない)

詰まったこと / 気づき:

  • json.dumps のデフォルトは , : でスペースを入れる。PHP json_encode 互換にするには separators=(',', ':') 必須
  • ただし日本語の出力サイズは v2 (UTF-8 そのまま) < v1 (\uXXXX エスケープ) で v2 の方が約 5% 小さい
  • csv.writer の lineterminator="\n" を明示しないと OS によって CRLF が入る可能性あり、明示しておく方が安全
  • aioboto3 の Session() は SDK デフォルトの認証チェーン(環境変数 → ~/.aws/credentials → IAM Role)を辿るため、コード側で profile 名を埋めなくて良い

次回やること:

  • v2 を Lightsail に新規構築してデプロイ動作検証
  • 本番用の最小権限 IAM ユーザー作成(s3:PutObject on schulte-thomsons だけ)+ アクセスキー発行
  • ALB taki から schulte TG を外す or DNS 切り替えで v2 にトラフィックを寄せる手順設計

16:51 - v2 に残り API 3本実装 (createPlayer / updatePlayerName / downloadRankingTsv)

やったこと:

(1) v1 の3エンドポイント仕様再確認 (schulte-server/app/Controller/V1Controller.php)

  • createPlayer: name 受け取り → 重複チェック → 7文字 ID 生成 → INSERT
  • updatePlayerName: id name 受け取り → 同名の他プレイヤー除外チェック → INSERT ON DUPLICATE KEY UPDATE
  • downloadRankingTsv: パス引数 mode/term → TSV 1000行 + Content-Disposition

(2) v2 に実装 (app/main.py 追記)

  • POST /v1/createPlayer (Form: name)
    • 重複時は errorCode=1
  • POST /v1/updatePlayerName (Form: id, name)
    • 同名の他プレイヤー時は errorCode=1
    • 自分自身が同名の場合は通す(v1 と同じ挙動)
  • GET /v1/downloadRankingTsv/{mode}/{term} (Path)
    • term: 1=daily, 2=weekly, 3=monthly, 4=total
    • Content-Disposition: attachment; filename="modeN_<term>.tsv"

(3) 本番 RDS で動作確認 (7ケース)

  • createPlayer 新規 / 別名 / 重複 → errorCode=1
  • updatePlayerName リネーム成功 / 他人と同名 → errorCode=1
  • downloadRankingTsv mode=4 term=2: 1000行 TSV
  • downloadRankingTsv mode=5 term=1: 1000行 TSV
  • DB 確認で -renamed の name 反映を確認、テスト用 player 2件削除

(4) v2 commit & push

詰まったこと / 気づき:

  • v1 の Util::generateRandomId(7) は7文字ランダム ID 生成。v2 でも random.choices(ascii_letters + digits, k=7) で同じ仕様
  • v1 の insertOnDuplicateKeyUpdate を SQLAlchemy 2.x で再現するには mysql_insert(...).on_duplicate_key_update(...) が綺麗
  • TSV 出力は FastAPI の Response で media_type="application/octet-stream" + Content-Disposition ヘッダで行ける(StreamingResponse は不要、1000行程度なら一括返却で十分)
  • curl -I (HEAD) を GET エンドポイントに送ると 405 が返る。GET のヘッダ確認は curl -D - -o /dev/null を使うべき

次回やること:

  • バッチ系 2本実装: uploadRankingTsvToS3, uploadRankingScoresAndPlayersJsonData(aioboto3 で S3 連携)
  • S3 認証情報の準備をどうするか相談(.env に IAM アクセスキー / Lightsail 上の IAM 経由 / 直書き等)

16:41 - DB スキーマ吸い出し + v2 に DB 接続 + sendScore / sync 実装

やったこと:

(1) DB スキーマ吸い出し

  • 1号機経由で本番 RDS (footballnext / MySQL 8.4.8) のスキーマを取得
  • 当初ローカル mysql 9.6 + SSH トンネル経由で接続を試みたが Access denied で失敗(mysql 9 系クライアントの認証方式と RDS の組み合わせで弾かれた)
  • 1号機内 PHP CLI から mysqli 経由で接続成功 → SHOW CREATE TABLE を全テーブル分実行 → ローカルの schulte-server-v2/schema/init.sql に保存
  • 全10テーブル: cake_sessions / connection_test / mode4-7_scores / players / test / test_players / users
  • スコア合計 約380万件、プレイヤー 6.3万人。AUTO_INCREMENT は mode5_scores が最大の 1,755,789

(2) v2 に DB 接続実装

  • 依存追加: aiomysql, sqlalchemy[asyncio], greenlet, cryptography, pydantic-settings, python-multipart
  • Python 3.13 固定(.python-version 配置、最初は uv が 3.14 を選んでた)
  • app/config.py: pydantic-settings で .env 読み込み
  • app/db.py: SQLAlchemy 2.x async engine + sessionmaker + DI 用 get_session
  • app/models.py: Player, Mode4-7Score(共通 Mixin で4テーブル定義)
  • app/main.py: lifespan で engine.dispose、/healthcheck/db で MySQL バージョン取得確認
  • .env に SSH トンネル経由 RDS 接続用の値を設定(gitignore 済み)
  • .env.example をサンプル値で配置

(3) /v1/sendScore 実装

  • INSERT IGNORE で unique_key 重複検知
  • 重複時は既存 playerId を返す
  • playerId 空のときは7文字ランダム生成 + players に INSERT
  • del カラムは Python 予約語回避のため del_ でマップ
  • 異常系(score=0 / unique_key="")は isSuccessed: false で即返却

(4) /v1/sync 実装

  • 直近1週間 (timestamp >= now - 1week) かつ del=0 のスコアを取得
  • スコア昇順 (速い順) で 500 件
  • Player JOIN で名前取得
  • isSmart=true で軽量化形式(players は dict、scores は配列の配列)

(5) 本番 RDS で動作確認

  • /v1/sync?mode=4: 37 players / 500 scores 取得成功(実プレイヤー名・絵文字含む日本語も正常)
  • /v1/sendScore 4ケース成功:
    • 新規 playerId=9HAqPDI 生成
    • 同じ unique_key 再送 → 既存 playerId 返却(重複検知)
    • 別 unique_key + 既存 playerId で2件目スコア記録
    • 異常系(score=0, unique_key 空)は拒否
  • テストレコード(unique_key LIKE 'v2-test-%')は1号機経由 PHP で削除済み、本番 DB は元通り

(6) v2 commit & push

詰まったこと / 気づき:

  • ローカル mysql 9.6 クライアントから RDS 接続が Access denied で失敗。原因は MySQL 9 系クライアントの認証プラグイン挙動。aiomysql (PyMySQL ベース) は問題なく接続できたので開発に支障なし
  • PHP 5.3 は [] 短い配列構文や func()[0] の即時参照が NG(5.4+)。1号機内 PHP スクリプトは array() と一旦変数代入で書く必要がある
  • uv init --python 3.13 してもローカル Python 3.14 が使われた(requires-python = ">=3.13" だと 3.14 もマッチ)。.python-version で固定するのが確実
  • sendScore のテストは「本番 RDS に v2-test- prefix の unique_key で送り、後で削除」運用を採用。今後の動作確認でも踏襲

次回やること:

  • 残り API 3本実装: createPlayer, updatePlayerName, downloadRankingTsv(DB 操作のみ)
  • バッチ系2本実装: uploadRankingTsvToS3, uploadRankingScoresAndPlayersJsonData(aioboto3 で S3 連携、認証情報の準備が要る)

15:50 - Lightsail 移行検証 → schulte-server-v2 への方針転換 + 最小スケルトン作成

やったこと:

(1) Lightsail 1台移行に耐えられるかの事前検証

  • ALB taki が schulte 含む5アプリ共有と判明(baseballnext / footballnext / machiga / schulte / sposhin)
  • schulte EC2 2台 (t3.nano) のメトリクス取得: CPU 平均 0.85% / 最大 3.6%、月転送量 約 8GB → 性能的には Lightsail 1台で完全に耐えうると判定
  • ALB schulte TG リクエスト数: ピーク 0.33 RPS(2台合計)、5XX エラーは7日間ゼロ
  • RDS footballnext: ピーク CPU 56%、Freeable Memory 最低 54MB(他アプリ込み、要注意)

(2) EC2 詳細調査(深刻なレガシー発覚)

  • OS: Amazon Linux 1 (2018.03)、AWS サポートは 2023-12 で終了済み
  • PHP 5.3.29 (2014年 EOL)、Apache 2.2.34、CakePHP 2.0.4(2011年)
  • メモリ余裕無しの原因 = mod_php5 prefork で httpd 各 child 約45MB × 9 = 392MB / 461MB を占有
  • swap 84MB 使用中、CodeDeploy/CloudWatch Agent 等の常駐サービスが追加で約 130MB

(3) PHP-FPM 化(メモリ改善策)の検討と中断

  • Apache 2.2 には mod_proxy_fcgi が無いため、選択肢: α (mod_fcgid + php-cgi) or γ (httpd24 + mod_proxy_fcgi + php-fpm)
  • 検証用に2号機を ALB から detach
  • AppName タグを Schulte → Schulte_maintenance に変更し Lambda 自動 attach を回避(後述)
  • 後段の方針転換でこの作業は中断

(4) Lambda 監視機構の発見

  • TCM_observeALB_startEC2 / attachToTargetGroup / detachEC2FromTargetGroup の3つが per1min で動作
  • ALB から単純に detach すると、最大1分以内に attach Lambda が再 attach する仕組み
  • 回避策として AppName タグ書き換えで Lambda の対象から外す方法を採用(v2 路線への切替時にも有効)

(5) 大方針転換: schulte-server-v2 構想

  • レガシー(PHP 5.3 / CakePHP 2.0.4)継承を断念し、Python / FastAPI でゼロから書き直す方針
  • schulte-server の API 規模感把握: 実 API 8個(sendScore, createPlayer, updatePlayerName, sync, downloadRankingTsv, uploadRankingTsvToS3, uploadRankingScoresAndPlayersJsonData, healthcheck)
  • 認証なし、外部連携 = S3 のみ(バケット schulte-thomsons、public-read)
  • ランキングは S3 に TSV 公開、クライアントは S3 から直接取得 → 低 EC2 負荷の理由
  • cron は別サーバー(cron EC2 インスタンス)から HTTP で 20 ジョブ叩く構成(cron.txt 確認)
  • DB スキーマは CakePHP Schema には無く、実 RDS から SHOW CREATE TABLE で取得が必要

(6) schulte-server-v2 リポジトリ作成

  • ローカル: ~/cdev/schulte-operation/schulte-server-v2/
  • GitHub: ichirokisanuki/schulte-server-v2 (private)
  • スタック: Python 3.13 + FastAPI + uv + uvicorn + aiomysql + SQLAlchemy 2.x (async) + aioboto3 + Pydantic v2
  • 最小スケルトン (/healthcheck, /docs) ローカル動作確認済み (HTTP 200)
  • 初回コミット e9d63d8 push 済み
  • operation 側の .gitignore に /schulte-server-v2/ 追加

(7) 2号機停止

  • AppName タグは Schulte_maintenance のまま、EC2 stop 実行
  • Lambda 監視対象外なので自動再起動されない、本番は1号機のみで継続稼働中

決めたこと:(→ DECISIONS.md 参照)

  • Python / FastAPI で v2 を書き直す
  • v2 は operation 直下に物理配置、別 GitHub リポジトリ管理
  • v2 の開発メモは operation/.devnotes/ に集約(v2 側には .devnotes/ を置かない)
  • PHP-FPM 化検証は中止

詰まったこと / 気づき:

  • ALB taki が共有 ALB だった。v2 移行時には他4アプリへの影響を考慮した経路変更が必要
  • Lambda 自動 attach/start/detach 機構が存在し、運用変更時には AppName タグで監視対象から外す必要
  • メモリ問題の本質は OS/PHP/Apache のレガシー構成。v2 で全部刷新すれば自然に解消する

次回やること:

  • DB スキーマを実 RDS (footballnext.ckdbljv81o1g.ap-northeast-1.rds.amazonaws.com) から SHOW CREATE TABLE players, mode4_scores, ... で吸い出す
  • v2 に DB 接続 (aiomysql + SQLAlchemy async) 実装
  • v2 に sendScore sync を実装

13:11 - プロジェクト初期セットアップ(概要記入+直近ロードマップ)

やったこと:

  • リポジトリ直下に物理配置する schulte-server/ schulte-unity/ を .gitignore に追加(先頭スラッシュ付きでルート直下のみ対象)
  • CLAUDE.md の「プロジェクト概要」「開発メモ」を埋めた
    • アプリ概要(iOS/Android リリース済み、App Store / Google Play リンク)
    • 構成(Unity クライアント + PHP バックエンド、EC2 nano × 2台 + LB、RDS は footballnext インスタンスに間借り)
    • schulte-server / schulte-unity は別管理である旨
  • .devnotes/ROADMAP.md の「今月」に直近やりたいこと3点を追記
    • インフラ縮小(EC2 nano × 2台 → Lightsail 1台)
    • schulte-server を CodeCommit → github.com に移行
    • schulte-unity を CodeCommit → github.com に移行(重そうなので要検討)
  • ユーザーレベルメモリーに project_overview.md と roadmap_2026q2.md を保存し、MEMORY.md インデックスを作成

決めたこと:

  • schulte-operation は「運用リポジトリ」と位置付け、実装ソース(schulte-server / schulte-unity)は同居させず別リポジトリで管理する(→ DECISIONS.md 参照)

次回やること:

  • ロードマップ3項目のうち、どれから着手するか方針決め
  • schulte-server の GitHub 移行に着手する場合、まず CodeCommit 側のリポジトリサイズと現状ブランチ構成の確認

最近のコミット

README

schulte-operation

概要

(記入予定)

セットアップ

(記入予定)

使い方

(記入予定)