7channel-operation
WIP(現在進行中)
Work In Progress
このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。
現在の状況
記事日付バグ修正+記事一覧レイアウト改善を反映し iOS 8.3.2 審査提出(審査結果待ち)
① iOS 8.3.2 審査提出(2026-06-18・結果待ち)
8.3.1 以降の蓄積をまとめて 8.3.2 を App Store 審査に提出。リリースノートは「・視聴履歴機能を追加 / ・アプリの安定性を向上しました」。含まれる主な変更: 視聴履歴タブ追加・記事一覧の画像なしレイアウト改善・番組0件チャンネルのタップ直行・YouTube埋め込み自動再生オフ・try?化のクラッシュ対策。審査結果待ち。 ※7channel-ios は CodeCommit(master)管理、push は AWS_PROFILE=ichirokisanuki git push origin master。
② 記事日付の1970/01/01バグ修正(2026-06-18・サーバ側で全ユーザー即時反映済み)
記事一覧で日付が 1970/01/01 の記事が上位に出る不具合を修正。真因は API のシリアライズ形式(isoformat()=T+ハイフン)と iOS パーサ(yyyy/MM/dd HH:mm:ss)の不一致で、パース失敗→Realm既定値エポックに落ちていた(DBは正常)。api/common.py serialize_dates を %Y/%m/%d %H:%M:%S 形式へ修正+cron/article_crawl.py のフォールバックをエポック→datetime.now() に変更。本番反映済み(アプリ更新不要で全ユーザーに効く)。movies の upload_date も同経路で是正。
③ 出演者関連付け自動化+mcr一本化(2026-06-16 完成・稼働中)
cast_matcher.py が Codex CLI で出演者IDを判定し movie_cast_relations(mcr) へ書き込み。launchd 30分毎(Mac実行・13308トンネル経由)。watermark方式(cast_matcher_state.json)で新着のみ判定。⚠️Codex認証がMac側のみ=Mac停止中は新着にキャスト付かない(次回launchdで復旧)。関連テーブルは cmr→mcr に一本化(段1〜3完了、UNIQUE(movie_id,cast_id)付与済み)、段4(cmr DROP)は観察期間後で未実施。
④ 継続中(前セッションから)
- AdMob 制限解除の審査結果待ち(〜6/19頃)。解除後 rate 見直し(バナー/ネイティブをAdMob優先に戻すか)。
- コンテンツ大量拡充(稼働121ch)後の様子見。消滅疑い3ch(id12/36/98)・del=1番組を指す生存動画1,563本の棚卸し判断。
次にやること:
- iOS 8.3.2 の審査結果確認+本番実データ確認(視聴履歴の利用・Crashlytics・記事日付が直ったか)
- サーバー側 Codex 化(取り込み直後リアルタイム発火=Mac依存解消)
cast_movie_relationsの DROP(観察後・段4)knowledge/cast_matching_hints.mdの育成 / (継続)AdMob審査結果・棚卸し
v3 のデプロイ方法(メモ): scp -i ~/.ssh/lightsail-7channel.pem <files> ubuntu@35.73.116.227:~/7channel-server-v3/ → sudo systemctl restart 7channel-web(cron/ や api/ はパス指定でscp。repoの deploy.sh は古いので使わない)
過去のWIPアーカイブ
2026-06-18 06:00 時点アーカイブ(出演者自動化稼働・mcr一本化 / 8.3.2提出前)
出演者関連付けの自動化(Codex判定)稼働+関連テーブルを movie_cast_relations に一本化
① 出演者関連付け自動化(2026-06-16 完成・本番反映)
cast_matcher.py が Codex CLI で動画タイトル/概要+出演者マスタ(名前/別名 other_matching_name/除外 no_matching_name)から出演者IDを判定し movie_cast_relations へ書き込む。launchd で30分毎(Mac実行・13308トンネル経由、com.7channel.cast-matcher)。
- watermark方式(
cast_matcher_state.json, Mac local)で新着のみ古い順に1回ずつ判定。(なし)動画も前進させ再判定ループ無し。判別=「movie.id<=watermark なら判定済み(出演者有無は別)」。 - 取り込み時(autoEntry)の番組ルール固定キャスト付与を撤去→出演者は Codex が唯一の決定者。
- 誤判定対策の知識は
knowledge/cast_matching_hints.md(人間がメンテ)に1行追記すると次回から反映=実質の学習。 - ⚠️トレードオフ: Codex認証がMac側のみ=Mac停止中は新着にキャスト付かない(次回launchd起動で復旧)。最終ゴールはサーバー側Codex化でリアルタイム発火。
② 関連テーブルを mcr に一本化(段1〜3完了・段4未)
cast_movie_relations(cmr)/movie_cast_relations(mcr) の二重持ち(同じmovie↔cast情報, キーがyoutube_code vs movie_id)を mcr に統一。readers(api/common.py, s3_uploader.py)・writers(autoEntry, database.py, admin, cast_matcher)・relation_sync を全部 mcr へ。mcr に UNIQUE(movie_id,cast_id) 付与済み。cmr は孤立・凍結、段4(cmr DROP)は観察期間後。旧PHP(7channel-server)は休眠なので影響なし。
③ 継続中(前セッションから)
- AdMob 制限解除の審査結果待ち(〜6/19頃)。解除後 rate 見直し(バナー/ネイティブをAdMob優先に戻すか)。
- コンテンツ大量拡充(稼働121ch)後の様子見。消滅疑い3ch(id12/36/98)・del=1番組を指す生存動画1,563本の棚卸し判断。
- iOS 8.3.1 本番実データ確認(ATT許諾率・Crashlytics・Zucks収益・GA4 screen_view)。
次にやること:
- サーバー側 Codex 化(取り込み直後リアルタイム発火=Mac依存解消)
cast_movie_relationsの DROP(観察後・段4)knowledge/cast_matching_hints.mdの育成(誤判定を見つけたら1行追記)- (継続)AdMob審査結果・大量追加後の様子見・棚卸し・ドキュメント実態合わせ
2026-06-16 12:17 時点アーカイブ(コンテンツ拡充投入・admin保護・AdMob審査待ち / 出演者自動化の前)
コンテンツ大量拡充完了(121チャンネル・movies +16,749本/日)+ admin 認証保護 + AdMob 審査結果待ち
① コンテンツ拡充(2026-06-13 実運用投入完了)
チャンネル管理(前日完成)を実運用に投入。YouTube 検索で発掘した未登録候補102件からユーザーが大量追加し、稼働121チャンネル・movies は1日で+16,749本。パイプラインはバックログ0で健康。運用中に出た課題も全部修正済み:
- 番組編集の動画検索をチャンネル内に限定
- 過去動画の遡り上限 2000→1万本(5,071本を37秒で実測)、Nginx /admin タイムアウト300s
- 管理画面の動画検索を FULLTEXT→LIKE(「ミリオン★タッグ」OR検索化問題の根治)
- チャンネルアイコンのリンク切れを日次cron
/cron/refreshChannelImages(4:15 JST)で自動修復。初回99件修復 - ⚠️ チャンネル消滅疑い3件(id12 打ってけ!TV / id36 D'STATION TV / id98 キクヤチャンネル)→ 棚卸し対象
② admin セキュリティ保護(2026-06-13・サーバー側のみ)
/admin が認証ゼロの完全公開だったのを発見し保護:
- Nginx Basic 認証(ユーザー名
7ch、htpasswd/etc/nginx/.htpasswd-admin、パスワードはパスワードマネージャー保管) - cron系(/cron・/crawl・/api3/updateHotMovies・/api2/beginBroadcast)外部403遮断、crontab は 127.0.0.1 直叩きに統一
- iOS API は無影響を検証済み。Nginx バックアップ:
7channel.conf.bak-20260612
③ iOS 8.3.1 公開済み・AdMob 制限解除の審査中(〜6/19頃)
8.3.1 は App Store 公開済み(2026-06-12 承認)。AdMob 制限解除の審査リクエスト提出済み。広告は全面 Zucks 直接配信中(バナー _50d0640137 / ネイティブ _cc69bfb840 / レクタングル _6ba8332444、インタースティシャル廃止)。
次にやること:
- AdMob 審査結果待ち。解除されたら rate 見直し(ネイティブ/バナーを AdMob 優先に戻すか。レクタングルは AdMob 禁止枠なので Zucks のまま)
- 大量追加後の様子見: curate 採用通知のノイズ、program_id=0 動画の量、アプリの見え方
- 消滅疑い3チャンネルの棚卸し(チャンネル・動画ごと論理削除するか判断)。関連: del=1 の番組を指したまま生存している動画1,563本(program1 閉店くんが行く=1265 / program132 打ってけ!TV=298)の扱いを判断(2026-06-16 発見・別件)
- ※2026-06-16 完了: is_single_program チャンネルの同名番組を全21件撤廃し動画3,688本を program_id=0 に(動画詳細の冗長表示を解消・サーバ/本番データ/iOS。詳細はDEVLOG)
- 8.3.1 本番の実データ確認: ATT 許諾率・Crashlytics クラッシュ・Zucks 収益数値・GA4 screen_view
- 旧v2 Lightsailインスタンス(13.230.63.19)の棚卸し・削除 →【誤認だった・訂正済み】13.230.63.19 は現役の多目的サーバー(mydb.ikapps.com)本体。削除厳禁(下記⚠️訂正参照)
- ドキュメント実態合わせ(
multi-purpose-lightsail-server1README / v3deploy.sh)
v3 のデプロイ方法(メモ): scp -i ~/.ssh/lightsail-7channel.pem <files> ubuntu@35.73.116.227:~/7channel-server-v3/ → sudo systemctl restart 7channel-web
2026-06-13 07:08 時点アーカイブ(チャンネル管理完成直後・実運用投入前)
v3 admin にチャンネル管理を新設(2026-06-12)。/admin/channels/new は YouTube の URL/@ハンドル/動画URLを貼るだけでチャンネル登録(API自動解決)。チャンネル編集画面から過去動画をチャンネル内検索して取り込み(program_id=0)→ 番組編集画面で割り当ての2段構え。upload_unixtime=実投稿日時の修正済み。初の運用実績は「スロぱちの隣駅」(id=112)。
2026-06-12 18:07 時点のスナップショット(8.3.1 公開直後・チャンネル管理実装前)
iOSアプリ 8.3.1 は App Store 公開済み(2026-06-12 承認)。広告クラッシュ・ポリシー違反・Zucks直接化・ATT・YouTube自動再生オフをすべて含む。同日、AdMob ポリシーセンターから制限解除の審査リクエストを提出済み(問題=「サイトの動作: ナビゲーション」、修正=インタースティシャル完全削除、審査は通常1週間程度)。広告配信状態: バナー Zucks _50d0640137(需要割り当て待ち)、ネイティブ Zucks _cc69bfb840、レクタングル Zucks _6ba8332444、インタースティシャル廃止、AdMob は解除審査中。
2026-06-12 時点のスナップショット(8.3.1 提出直前)
iOSアプリ 8.3.1 の実装は全完了、Archive → App Store 提出待ちの状態。実装内容: AdMob XIBクラス名修正(GADUnifiedNativeAdView→GADNativeAdView)、スクロールクラッシュ修正(topViewController→viewControllers.first)、インタースティシャル廃止、SKAdNetworkItems 50エントリ、ATT許可プロンプト、Crashlytics dSYM自動アップロード、バナー/ネイティブ/レクタングルのZucks直接化、YouTube自動再生オフ、バージョン8.3.1統一。提出後に AdMob「審査プロセスを開始」を実行する段取りだった。
2026-06-11 時点のスナップショット(8.3.0リリース直後・広告リワーク着手前)
プロジェクト概要
7channel の運用を管理するモノレポ的な構成。
7channel-ios/— iOSアプリ(別gitリポ=AWS CodeCommit、このリポからはgitignore。git操作はAWS_PROFILE=ichirokisanuki必須)7channel-server/— レガシー(PHP 5.3 + CakePHP)。現在は休眠(本番は v3)7channel-server-v3/— 本番。iOSアプリAPI + cron系を Python(FastAPI) で実装、Lightsail Micro 1台に集約
現在の状態(2大トピック)
① iOSアプリを Xcode 26 向けに近代化 → App Store 審査提出済み(2026-06-07・結果待ち)
塩漬け(2020年構成)で最新Xcodeでビルド不能だったのを全面近代化し、実機起動 → アップロード → 年齢制限/輸出コンプライアンス/リリースノートを片付けて審査提出まで完了。あとは Apple の審査結果待ち。年齢制限は新方式で 18+(疑似ギャンブル頻繁のため。実ギャンブルは「いいえ」)。詳細は DEVLOG 2026-06-07。
- 依存全面更新: AdMob 7.62→13.4 / Firebase 6→12 / Alamofire 4→5 / Realm 5→20 / OneSignal 2→5、deployment target iOS 15
- 広告をAdMobに集約(終了済みの nend / i-mobile 撤去)=レビュー改善案①
- 画面計測を復活(
sendScreenView→Firebasescreen_view、GA4の(not set)解消)=改善案② - バージョン 8.3.0、本番Team JFBRCYHN24(Thomson Inc.) 署名でアップロード済み
- ⚠️ アカウント移管(HV2598SQ7Y→JFBRCYHN24)に伴うキーチェーン警告あり=既存ユーザーは過去の自分のタグ/コメントを編集/削除不可になる(実害小・回避不能)
- コミット: iOSリポ
master09a98bf まで push 済み
② サーバー = Lightsail Micro 1台に完全集約(2026-05-04 完了・安定稼働中)
本番構成 iPhone → 7channel.ikapps.com → Lightsail Micro (Nginx 443 SSL → FastAPI v3 + MySQL同居)、月約 $7($106→$7、年-$1,188削減)。cron は Micro内に統合。詳細は下記アーカイブ+ DEVLOG 2026-05-04。
- 管理画面URL(現行)=
https://7channel.ikapps.com/admin(HTTPS)。旧7ch.ikapps.com(HTTP) はDNS消滅で到達不能(ドキュメント側が古いだけ)。 - ⚠️**【2026-06-15 訂正】
13.230.63.19(静的IP7channel-v2-ip)は放置インスタンスではなく現役の多目的サーバー本体**。mydb.ikapps.com(phpMyAdmin)+ footballnext/baseball/7channel の SSHトンネル+他プロジェクトのサービスが稼働中。ssh -i ~/.ssh/lightsail-multipurpose ubuntu@13.230.63.19。削除・静的IPリリース厳禁(やると多目的サーバーごと全滅)。7channel本体は別インスタンスの Micro35.73.116.227。- 7channel DB を phpMyAdmin で見る経路 = mydb 上の systemd
phpmyadmin-tunnel-ch7.service(127.0.0.1:13308 → ssh Micro → 127.0.0.1:3306)。2026-06-15 にこのトンネルを旧経路(削除済みkizuna RDS / 終了済み踏み台ch7 EC2)から Micro 直結へ張り直して復旧。
- 7channel DB を phpMyAdmin で見る経路 = mydb 上の systemd
③ iOS: YouTube埋め込みをアプリ内インライン再生化(2026-06-10)
動画詳細のYouTube埋め込みがタップで外部YouTubeアプリに飛ぶ問題を修正。AdvancedWebView.swift(WKWebViewにallowsInlineMediaPlayback=true)+YoutubePlayerView.swift(playsinline=1、baseURL/originを自前ドメイン7channel.ikapps.comで一致)で解決。エラー153→152→成功の経緯はDEVLOG 2026-06-10参照。iOSリポ未push分はこのセッションでコミット予定。
次にやること
- App Store 審査結果を待つ(リジェクトされたら理由に対応。UGCの通報/ブロック不足を突かれる可能性に留意)
- 承認後: 本番での広告表示・画面計測(GA4 screen_view)・クラッシュ(Firebase Crashlytics)が正しく動いているか実データで確認
- iOS フォローアップ(ROADMAP参照): ネイティブ広告XIBの旧クラス名(
GADUnifiedNativeAdView)確認、OneSignal通知拡張のリッチ通知再統合、ATT許可プロンプト実装 - レビュー(
review-20260606)の高優先: 広告収益化の実数把握(面別eCPM)、広告除去サブスク検討 - 旧v2インスタンス(13.230.63.19)の棚卸し・削除 →【誤認・2026-06-15訂正済み】13.230.63.19 は現役の多目的サーバー(mydb.ikapps.com)本体。削除厳禁(上の⚠️訂正参照)
- ドキュメント実態合わせ(lightsailサーバーリポのREADME/.devnotes、v3 deploy.sh の古いURL/IP修正)
リバート手順(サーバー)
- 何かあれば Lightsail のスナップショットから復元 or サーバー内で問題切り分け
- v3 のコード変更でロールバックしたい場合は git で前バージョンに戻して rsync デプロイ
- iOS をロールバックしたい場合は CodeCommit で前コミットに戻して再アーカイブ
過去のWIPアーカイブ
2026-06-07 時点のスナップショット(サーバー集約完了・iOS近代化前)
サーバーは Lightsail Micro 1台に完全集約済みで安定稼働(月$7)。DB設定(utf8mb4化・JST化・YouTube APIキー3本分散)、SNS通知(SMS_Alert を Ichiro IAM User で publish、subscriber は goda/ichirokisanuki.apple)、ローカル開発(MariaDB + Docker PHP)など詳細は DEVLOG 2026-05-04 群を参照。積み残しだった CloudFront ECQ9QLPK0563T 削除・RDS最終スナップ kizuna-final-before-delete-20260504 削除は dev-timer #12/#13 で対応予定だった(要最終確認)。この時点までサーバー側が主戦場で、以降は iOS アプリ近代化に移行。
2026-05-04 19:30 時点のスナップショット
v2 (7channel-server-v2) を v3 に完全吸収完了。cron/curator.py を FastAPI ルーター /cron/curate として新規実装し、Lightsail Micro の crontab に登録。SNS publish も Ichiro IAM User で動作確認([TEST] メール到着確認)。多目的Lightsail から v2 cron / systemd / ディレクトリ / Nginx 設定 / Route53 A レコードまで全部撤去。ローカルも git rm 済み。7channel = Lightsail Micro 1台に完全集約。
2026-05-04 18:50 時点のスナップショット
完全別鯖から叩いていた v3 cron 16 行を 7channel-server-micro の ubuntu crontab に移植。MAILTO抑止+ >/dev/null 2>&1 付与。syslog/access.log で発火確認、別鯖側は停止済み(旧別鯖自体は別用途で継続稼働)。これで 7channel 関連 cron は Lightsail 2 台(Micro が v3 cron / 多目的が v2 curate.py)に集約完了。
2026-05-04 18:30 時点のスナップショット
cutover後に潜んでいた隠れバグを総ざらいで対処。① v3 cron が MySQL strict mode で500連発していたのを4ファイル修正、② YouTube quota 超過で「全動画削除済み」誤判定の連鎖を新APIキー(別プロジェクト)+コード防御で解消、③ ローカル PHP テスト用 MariaDB 立ち上げ+スキーマ流し込み、④ movie_uploads/movies を utf8mb4 化(絵文字対応)、⑤ Lightsail OS+MySQL を JST 化+ publishedAt の JST 変換、⑥ v2 curate.py の DB_HOST が削除済み kizuna のままで沈黙していたのを修正。これで完全復活。
2026-05-04 17:30 時点のスナップショット
最終ゴール達成済み(同日 13:40 のスナップショット)に追加で、AWSアカウントに残っていた古いEBSスナップショット 12個(96GB)と AMI 11個を全削除。13年前の旧サービス(FootballNEXT / bizpice / news / haiena / 初代7channel等)の置き土産まで完全クリーンアップ。月-$4.8。RDS最終スナップ 9GB は保険として1週間後(5/11)に削除予定、dev-timer #13 で予約。
2026-05-04 13:40 時点のスナップショット
最終ゴール達成。RDSを Lightsail内部のMySQLに移行(kizuna削除)、CloudFront廃止して Lightsail で Let's Encrypt直接HTTPS化。月コスト$106→$7、年-$1,188削減。CloudFrontは Security Bundle 課金で月末まで削除不可、disable状態で放置(影響なし、6/1にリマインド設定)。
2026-05-04 05:50 時点のスナップショット
段階3 完遂。CloudFront origin を Lightsail に切替成功、iOSアプリ動作確認、B-6 (beginBroadcast/S3Uploader) 緊急実装、/v1/healthcheck/HEAD対応、EC2-2 terminate、old_movie_imps DROP、phpmyadmin の RDS接続を直接接続に変更(副次対応)。月コスト$106→$45(-$61/月、年-$732)。
2026-05-03 22:00 時点のスナップショット
Phase D 着手。Lightsail Nano 7channel-server 立ち上げ・v3 デプロイ完了・Nginx+systemd稼働。RDS Public化+SG許可で接続確立、外部疎通テストも PHP本番と一致確認。CloudFront origin 切替を試みたが「明日に進めたい」とユーザー判断で revert。明日 cutover 予定。
2026-05-03 19:50 時点のスナップショット
段階3 着手。v3 新規ディレクトリで Phase A〜C 完了(21メソッド実装+admin動作確認)。iOS API 9個 + cron 12個全部 PHP本番と動作一致をローカル検証済み。次は Phase D(Lightsail Smallにデプロイ)。B-6 (beginBroadcast) は保留。
2026-04-28 20:30 時点のスナップショット
段階2完了! CLB廃止・CloudFront経由HTTPS化・Lambda/EventBridge大掃除・EC2-1終了。月$33-37削減。残構成は EC2-2 + RDS のみ。STRAIGHT_JOIN による14秒問題はロールバックでデプロイ済み。次は段階3(RDS移行とアプリリプレース)。
2026-04-27 21:00 時点のスナップショット
スロークエリ第3弾(device_id等の絞り込みパターン)も対処完了。movies に3つの複合インデックス追加(device/channel/del_upload_unix)と CastMovieRelation::getRows の STRAIGHT_JOIN 書き換えをデプロイ。最大7.6秒のクエリが62msに。dev-timer明日11:00セット。
2026-04-27 17:00 時点のスナップショット
スロークエリ第2弾(新ワースト1〜3)も全部インデックス追加で解消。articles.url、movies.idx_program_del_upload、movies.idx_del_upload の3つを追加。元のワースト1〜3は前セッションのデプロイで完全消滅を確認済み。次は明日5:00の cleanMovieImps 初回実行確認と SG クリーンアップ。
2026-04-27 11:30 時点のスナップショット
段階1(EC2-2単体稼働)24h観察クリア。RDS/PHP最適化を一気に実施: スロークエリログ有効化、movies/articles/cast_imps のインデックス整備、cast_imps INSERT停止、movie_imps を直近3ヶ月にswap、自動クリーンアップcron追加、ワースト1〜3クエリ全部対処してデプロイ完了。次は数時間後にスロークエリログ確認。
2026-04-26 13:30 時点のスナップショット
7channel-server-v2移行・curate.py cron・SNSアラートメール全て稼働中。残課題は旧Lightsail廃止検討と 7ch.ikapps.com のHTTPS化。
2026-04-25 02:10 時点のスナップショット
7channel-server-v2/ 統合完了、lightsail-claude-avatar/ は破棄、curate.py cron復旧済み。残課題は旧Lightsail廃止検討と 7ch.ikapps.com のHTTPS化。
2026-04-24 移行完了直後のスナップショット
リポジトリ初期化・GitHub連携済み。7channel-server-v2 は多目的サーバーに移行完了。7channel-server-v2/ と lightsail-claude-avatar/ はローカルに存在するが未コミット(untracked)。次は両ディレクトリのコミット管理方針を決めること。
2026-04-24 時点のスナップショット
(以下、ディレクトリ構成からの推測)
git init したばかり。.gitignore に 7channel-ios/ と 7channel-server/ を除外設定済み。7channel-server-v2/ と lightsail-claude-avatar/ は未コミット状態。初回コミット & GitHub push、7channel-server-v2/ の整理が次のステップ。
(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)
ROADMAP(計画)
ロードマップ
今週
- 出演者関連付けの自動化(cast_matcher.py + Codex CLI判定 + launchd 30分毎・watermark方式)
- 取り込み時の固定キャスト付与を撤去(autoEntry)→ 出演者は Codex が唯一の決定者に
- 関連テーブルを movie_cast_relations に一本化(段1〜3: readers/writers/UNIQUE付与/relation_sync退役)
- 判定知見ファイル導入(knowledge/cast_matching_hints.md をプロンプトへ注入)
- サーバー側 Codex 化(取り込み直後にリアルタイム発火=Mac依存を解消)
- 段4: cast_movie_relations の DROP(観察期間後・cmrは現在孤立凍結)
- iOSアプリを Xcode 26 向けに全面近代化(依存更新・AdMob集約・画面計測復活)→ 実機起動&App Storeアップロード完了
- App Store 審査提出(年齢制限18+・輸出コンプライアンス・リリースノート記入)→ 提出完了・審査結果待ち
- App Store 審査結果の確認 → 8.3.0 承認済み(2026-06-09)
- 8.3.1 Archive → App Store 提出 → 審査通過・公開完了(2026-06-12)
- AdMob コンソールから制限解除の審査リクエスト提出(2026-06-12・結果は〜1週間)
- AdMob 制限解除の審査結果確認(解除されたら rate 見直しへ)
- Zucksバナー枠
_50d0640137の配信開始確認(新枠のため需要割り当て待ち) - 8.3.1 本番実データ確認(ATT許諾率・Crashlytics・Zucks収益・GA4 screen_view)
- 記事日付の1970/01/01バグ修正(API serialize_dates をスラッシュ式に統一+crawlフォールバックをepoch廃止/サーバ側で全ユーザー即時反映)
- iOS: 記事一覧で画像なし記事はサムネイル領域を畳んでタイトルを右端まで拡大
- iOS 8.3.2 審査提出(視聴履歴タブ・記事一覧改善・try?化クラッシュ対策ほか/RN「視聴履歴機能を追加/安定性向上」)→ 提出完了・審査結果待ち
- iOS 8.3.2 審査結果の確認+本番実データ確認(視聴履歴の利用・記事日付が直ったか)
- admin にチャンネル管理を追加(YouTube URL貼るだけで登録・番組自動作成・優先クロール)
- チャンネル編集画面に過去動画の検索・取り込み機能を追加(uploadsプレイリスト遡り・program_id=0登録)
- 自動キュレーションの upload_unixtime を実投稿日時に修正(新着フィード占拠バグ)
- チャンネル追加の実運用 → YouTube検索で候補102件発掘・大量追加完了(稼働121ch・movies +16,749本/日)
- 番組編集の動画検索をチャンネル内に限定
- 過去動画の遡り上限を1万本に拡大(Nginx /admin タイムアウト300s)
- 管理画面の動画検索を FULLTEXT→LIKE に修正(記号入り検索語のOR化問題)
- チャンネルアイコンのリンク切れ自動修復 cron(日次4:15、初回99件修復)
- admin に Basic 認証+cron系エンドポイントの外部遮断(完全公開状態だったのを保護)
- 消滅疑い3チャンネル(打ってけ!TV / D'STATION TV / キクヤチャンネル)の棚卸し・論理削除判断
- AdMob制限解除後のrate見直し(ネイティブ/バナーをAdMob優先に戻すかの判断)
- iOS: YouTube埋め込みのアプリ内インライン再生化(baseURL/originを自前ドメインで揃えてエラー153/152を解消)
- 旧v2インスタンス
13.230.63.19の棚卸し・削除 →【2026-06-15 誤認と判明・対応不要】13.230.63.19(静的IP7channel-v2-ip)は現役の多目的サーバー(mydb.ikapps.com)本体。削除厳禁(消すと phpMyAdmin・各DBトンネル等が全滅)。7channel本体は別の Micro35.73.116.227 - ドキュメント実態合わせ:
multi-purpose-lightsail-server1のREADME/.devnotes と v3deploy.shの古いURL/IP(7ch.ikapps.com・13.192.166.254・7channel-server-v2)を現行(7channel.ikapps.com/HTTPS)に修正 - iOS: ネイティブ広告XIBの旧クラス名(
GADUnifiedNativeAdView→GADNativeAdView)修正+実機表示確認済み - AWS残置スナップショット大掃除(古AMI 11個 deregister + EBSスナップ 12個 / 96GB 削除、月-$4.8)
- 5/11 (dev-timer #13) で RDS最終スナップショット
kizuna-final-before-delete-20260504削除(月-$0.86) - cutover後の隠れバグ総ざらい(v3 cron strict mode / YouTube quota / utf8mb3 / TZ / v2 DB_HOST)を完全復旧
- 完全別鯖から叩いていた v3 cron を Lightsail Micro の ubuntu crontab に統合
- v2 (
7channel-server-v2) を v3 に完全吸収(curate.py → /cron/curate、systemd/Nginx/Route53 撤去、ローカル削除) -
7channel-server-v2/とlightsail-claude-avatar/をこのリポジトリでコミット管理するか方針を決める(v2は統合、avatarは破棄) - AWSコスト削減 段階1: ALB配下のEC2を2→1台に減らす(EC2-1停止、AppNameタグ削除)
- cron頻度調整(
movieUploadsClassic5分毎、removeNotNeedMovieUploads30分毎) - スロークエリログ有効化(mysql84 / long_query_time=1)
-
cast_impsにcast_idインデックス追加 -
moviesの重複/無価値インデックス4つ削除 -
articles.done_image_scrapingインデックス追加(ワースト1対策) -
cast_impsへの INSERT 停止(読まれていないため) -
movie_impsを直近3ヶ月swap、old_movie_impsに旧データ退避 -
cleanMovieImpscron 追加(毎日5:00自動削除) - HotMonthMovie cron
/api3/updateHotMovies/3復活 - スロークエリ ワースト2対処(
updateCastMovieCountをfind('count', recursive=-1)に) - スロークエリ ワースト3対処(
ORDER BY DATEDIFF→ORDER BY upload_date DESC3箇所) - スロークエリログ再確認(数時間後)→ 旧ワースト1〜3 完全消滅を確認
- スロークエリ第2弾: 新ワースト1対処(
movies.idx_program_del_upload追加 → 5.3秒 → 8ms) - スロークエリ第2弾: 新ワースト2対処(
movies.idx_del_upload追加 → 1.7秒 → 21ms) - スロークエリ第2弾: 新ワースト3対処(
articles.idx_url追加 → 約2秒 → 2.7ms) - スロークエリ第3弾: device_id絞り込み対処(
movies.idx_device_del_upload→ 7.3秒 → 62ms) - スロークエリ第3弾: channel_id予防インデックス(
movies.idx_channel_del_upload) - スロークエリ第3弾: cast起点パターン対処(
movies.idx_del_upload_unix追加 +CastMovieRelation::getRowsSTRAIGHT_JOIN化、デプロイ済み) - スロークエリ第3弾: 効果確認+STRAIGHT_JOINで14秒問題発覚→自然JOINにロールバック・デプロイ
-
cleanMovieImpsの初回実行確認 - SSH SGクリーンアップ
- AWSコスト削減 段階2: CLB廃止 + CloudFront経由HTTPS(-$33-37/月、当初の-$35見込みと同水準)
- CMN_*Lambda 4個削除、EventBridge 9ルール削除
- EC2-1 terminate
今月
- 7channel iOSアプリの本番動作の継続観察
- iOS: OneSignal 通知拡張のリッチ通知(画像付きプッシュ)を再統合(
OneSignalExtensionを拡張ターゲットに正式リンク) - iOS: ATT 許可プロンプト実装(実機確認済み・8.3.1に含む)
- レビュー改善(高): 広告収益の実数把握(AdMob面別 eCPM/fill/impression)→ 収益化の目標値設定
- レビュー改善(高): 広告除去サブスク/買い切りの最小導入を検討(「広告ウザい」固定客向け第2の柱)
- レビュー改善(中): ASO刷新+アプリ内レビュー依頼(
SKStoreReviewController)復活(新規流入の底上げ) - 外形監視を UptimeRobot に移行(任意・優先度低)
今四半期
- PHP 5.3 + 旧CakePHP製の本番アプリのリプレース方針決定 → FastAPI/Python に移植(v3 新規実装で v2 を吸収する方針)
- 段階3 Phase A: iOS API 9メソッドを v3 で実装、PHP本番と動作一致確認
- 段階3 Phase B-1〜B-5: cron系 12メソッド実装(YouTube/RSS/scraping含む)
- 段階3 Phase C: v2 admin 機能を v3 で動作確認
- 段階3 Phase B-6:
/api2/beginBroadcastの S3アップロード処理(保留可) - 段階3 Phase D: Lightsail Nano($5/月)で
7channel-server立ち上げ + v3 デプロイ + Nginx + systemd 稼働 - 段階3 Phase D: kizuna RDS Public化 + Lightsail static IP を SG許可(VPC peering 不可のため)
- 段階3 Phase D: Route53
lightsail.7channel.ikapps.comAレコード作成 - 段階3 Phase D: CloudFront のオリジンを
lightsail.7channel.ikapps.comに切替(cutover完了) - 段階3 Phase D: B-6 (beginBroadcast/S3Uploader) を緊急実装(保留できない設計だった)
- 段階3 Phase D:
/v1/healthcheck追加、HEAD method対応 middleware - 段階3 Phase D: EC2-2 terminate / Elastic IP 解放 /
old_movie_impsDROP - 段階3 Phase D: phpmyadmin の kizuna接続を直接接続に変更(EC2-2 terminate の副次対応)
- 段階3 最終: RDS テーブル整理(cast_imps / article_imps / kabutan / twitter_follow_* DROP、約313MB削減)
- 段階3 最終: Lightsail Nano → Micro ($7) アップサイズ(MySQL同居でメモリ余裕確保)
- 段階3 最終: Lightsail に MySQL 8 セットアップ + kizuna RDS データ移行(mysqldump 経由)
- 段階3 最終: phpmyadmin の接続も Lightsail MySQL(Lightsail内部VPC 172.26.8.107)に切替
- 段階3 最終: kizuna RDS 削除(最終スナップショット作成→Deletion Protection OFF→delete)
- 段階3 最終: CloudFront 廃止 → Let's Encrypt + Nginx 直接HTTPS化(DNS-01 challenge)
- 段階3 最終: Route53 7channel.ikapps.com を CloudFront → Lightsail static IP に切替
- 6/1 (dev-timer #12) で CloudFront ECQ9QLPK0563T 削除再挑戦(Security Bundle が月末まで残る制約)
- iOSアプリの JSON デコード型確認(PHP=string と v3=int の差で問題ないか)
いつか
- Lightsail micro のスナップショット定期バックアップ運用(任意)
- 外形監視を UptimeRobot に移行(任意・優先度低)
- admin: 取りこぼし確認ビュー(program_id=0 で movies に入った動画の一覧→その場で番組割り当て)
- admin: チャンネル発見支援(YouTube Search API でキーワードから未登録チャンネルをサジェスト→ワンクリック登録)
- 広告除去サブスク/買い切りの導入(近い将来はやらない・頭の片隅ストック)
DECISIONS(意思決定)
意思決定記録
このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。
2026-06-18: 日付のAPI配信形式は yyyy/MM/dd HH:mm:ss(スラッシュ式)を正準とし、修正はサーバ側で行う
背景: 記事一覧の日付が 1970/01/01 になる不具合の真因が、v3 API の serialize_dates が isoformat()(Tセパレータ+ハイフン)を返す一方、iOS の既存パーサ(PrimitiveObject.init(json:) / UtilExtension.stringToDate)が yyyy/MM/dd HH:mm:ss しか受け付けず、パース失敗→Realm既定値エポックに落ちていたことだった。iOS側を直す案もあった。
決定: API の日付配信形式を %Y/%m/%d %H:%M:%S(date型は %Y/%m/%d 00:00:00)に統一し、サーバ側で修正する。iOSパーサ側は変更しない。
理由: ①サーバ修正なら全ユーザーにアプリ更新なしで即時反映できる(iOS修正は審査・普及待ちで取り残しが出る)。②旧PHP版API のレスポンスがスラッシュ区切りで、iOSパーサはそれに合わせて作られていた=スラッシュ式が歴史的正準。v3移植時に isoformat() にしたのが逸脱だった。今後 v3 API で日付を返す箇所は全てこの形式に揃える。
2026-06-16: 動画への出演者付与は Codex CLI による個別判定にする
背景: 従来の出演者付与は「番組ルール固定キャスト(auto_entry_programs / cast_program_relations)を動画の中身に関係なく機械的に付ける」方式で、ゲスト出演やレギュラー欠席回がズレていた。動画ごとにタイトル/概要を見て出演者を当てたい。
決定: cast_matcher.py が Codex CLI(codex exec)に出演者マスタ+動画タイトル/概要を渡して出演者IDを判定する。サーバーに Anthropic SDK を組み込む案は採らず、既にローカル認証済みの Codex を使う。cast_matcher は cast_program_relations(番組レギュラー)は参照しない。取り込み時(autoEntry)の固定キャスト付与は撤去し、Codex を唯一の決定者にする。
理由: Codex 認証(ChatGPT定額)が手元にあり追加コストが小さい。番組レギュラー注入は精度の足しになるが「番組固定を脱したい」目的に反するので不採用。固定付与を残すと Codex は追加しかできず誤付与を消せないため撤去。トレードオフ(Mac停止中は新着にキャスト付かない)は許容し、将来サーバー側Codex化で解消する。
2026-06-16: 出演者の関連付けは movie_cast_relations に一本化する
背景: cast_movie_relations(youtube_code基準)と movie_cast_relations(movie_id基準)が同じ movie↔cast 情報を二重に持ち、relation_sync cron で正→従にミラー(ラグ+取りこぼしバグ)していた。
決定: mcr を正テーブルに一本化。readers/writers/未付与判定を全て mcr に寄せ、mcr に UNIQUE(movie_id,cast_id) を付与、relation_sync を退役。cmr は孤立・凍結し、観察期間後に DROP(段4)。
理由: 二重持ちは v3 の書き込み経路では必然性が無くラグ・取りこぼし・二重ソースの保守コストだけ残っていた。旧PHPは休眠で cmr を読む生きた経路は無いため廃止可能。DROP だけは不可逆なので即時はやらず観察を挟む。
2026-06-16: cast_matcher の処理済み判別は watermark 方式(位置ベース)にする
背景: 出演者ゼロ(なし)の動画は mcr 行が付かないため「未付与」のままで、毎回再判定される無駄(ループ)があった。「この動画は判定済みか」を持つ必要があった。
決定: 処理済み最大 movie.id を cast_matcher_state.json(Mac local)に持ち、id>watermark を古い順に1回ずつ判定→(なし)動画も含めて前進。movies へのフラグ列追加(per-movie)は採らない。
理由: 「今後追加される動画だけ」というスコープに watermark がぴったりで、本番 movies テーブルへのスキーマ変更+バックフィルを避けられる。位置ベースの限界(ID順前提)は本用途では許容範囲。
2026-06-13: admin は Nginx Basic 認証で保護、cron 系エンドポイントは localhost 限定
背景: https://7channel.ikapps.com/admin が認証ゼロの完全公開で、チャンネル/番組/出演者の CRUD が誰でも実行できた。cron 系(/cron, /crawl 等)も公開で、連打されると YouTube API quota を浪費させられる状態だった。
決定: Nginx の /admin に Basic 認証(アプリレベルのログイン画面や IP 制限は採用しない)。cron 専用エンドポイントは Nginx で外部403遮断し、crontab は 127.0.0.1:8000 直叩きに統一。
理由: Basic 認証は15分で導入でき iPhone Safari でも運用に支障がない。アプリレベル認証は工数の割に防御力が大差なく、IP 制限はスマホ回線の変動IPと相性が悪い。cron は外部から叩く理由が一切ないので遮断が正解。
2026-06-13: 管理画面の動画タイトル検索は FULLTEXT ではなく LIKE を使う
背景: 「ミリオン★タッグ」のような記号入り検索語で、FULLTEXT(BOOLEAN MODE) のトークンが記号で割れて OR 検索になり無関係なタイトルがヒット。フレーズ化("...")すると今度は記号の位置情報ズレで実データ204件あるのに0件になった。
決定: 番組編集・出演者編集の動画検索は title LIKE '%q%' の厳密部分一致に変更。
理由: 管理画面の用途は「入力文字列をそのまま含むタイトル」が期待値。LIKE は18万行全体でも0.4秒・チャンネル内なら0.1秒で実用十分。FULLTEXT は記号入り日本語との相性が根本的に悪い。
2026-06-13: チャンネルアイコンのリンク切れは日次cronでURL再取得して直す
背景: チャンネル運営者がアバターを変更すると、保存済みの yt3.ggpht.com URL が無効になり画像が表示されなくなる。
決定: /cron/refreshChannelImages で全チャンネルの画像URLを YouTube API から日次で取り直す。S3 への画像キャッシュ(自前ホスティング)はしない。API がデータを返さないチャンネル(削除/BAN疑い)は自動削除せず報告のみ。
理由: channels.list は50件/1unit で121チャンネルでも1日3unit とタダ同然。S3 キャッシュは確実だが取得・配信の実装が重く、日次更新で実用上十分。誤BANからの復活があり得るので自動削除はリスク。
2026-06-12: 過去動画の取り込みは「チャンネル画面で program_id=0 登録 → 番組画面で割り当て」の2段構え
背景: チャンネルの過去動画をYouTube APIで検索して取り込む機能の置き場所。当初は番組編集画面に「検索→番組ID付きで取り込み」を作りかけた。
決定: チャンネル編集画面に「チャンネル内検索→取り込み(program_id=0)」を実装。番組への割り当ては番組編集画面の既存「過去の動画に番組を割り当て」(ローカルmovies検索)を使う。
理由: 既存機能と責務が重複しない(取り込み=チャンネルの仕事、割り当て=番組の仕事)。1チャンネルに複数番組があるケースで、一度取り込んだ動画を複数番組へ柔軟に振り分けられる。ユーザー指示による方針。
2026-06-12: movies の upload_unixtime は常に実投稿日時を使う(現在時刻は使わない)
背景: curate / autoEntryMovie が upload_unixtime=現在時刻 で登録していたため、新規チャンネル追加時にRSSで拾った過去動画(最大15本)が「たった今投稿された」扱いでアプリ新着フィードの上位を占拠した。
決定: 自動キュレーション・手動取り込みとも、movie_uploads / YouTube API の実投稿日時(JST)から unixtime を算出する。
理由: 新着フィードの時系列が正しく保たれる。通常運用(投稿から数分でクロール)では現在時刻とほぼ同じなので副作用なし。新規チャンネル追加時だけ正しく過去に並ぶ。
2026-06-12: 過去動画の遡りは search.list ではなく playlistItems を使う
背景: チャンネル内のタイトル検索に YouTube Data API の search.list(100 units/50本)を使うと quota を大量消費する。過去に quota 超過で全動画誤削除の事故もあった。
決定: uploads プレイリスト(UCxxx→UUxxx)を playlistItems(1 unit/50本)で全件取得し、タイトル絞り込みはサーバー側で行う。上限は40ページ=2000本。
理由: 2000本遡っても40 units と search.list 1回未満のコスト。タイトルの部分一致もYouTube検索の曖昧マッチより制御しやすい。
2026-06-11: YouTube動画詳細は自動再生しない(タップで再生開始)
背景: 動画詳細画面を開いた瞬間にYouTube動画が自動再生されていた。
決定: autoplay=1 → autoplay=0。ユーザーがタップするまで再生しない。
理由: 自動再生は意図しない通信・音声の発生につながる。UX改善としてユーザー要望を受けて変更。
2026-06-11: AdMob制限中はZucksに直接切り替え(フェールオーバーではなく)
背景: AdMob が「制限付きで広告配信中」で全枠 "No ad to show" 状態。当初は「AdMob失敗時にZucksフェールオーバー」を実装したが、そもそも AdMob から試す意味がない状態だった。
決定: バナー/ネイティブともにZucksを直接呼ぶ構成に変更。レート DB も AdMob=0 / Zucks=1000 に更新。AdMob制限解除後に DB を更新するだけで切り戻せる設計。
理由: 制限中の AdMob に毎回空振りリクエストを送るのは無駄。DB変更だけで比率を変えられるサーバー側制御の仕組みを活用。
2026-06-11: インタースティシャル広告を恒久廃止
背景: 動画詳細から戻るときにインタースティシャルを表示していたが、AdMob コンソールで「ナビゲーション操作時のインタースティシャル」としてポリシー違反と判定され、広告配信が「制限付き」に落とされていた。
決定: MovieDetailViewController のインタースティシャル実装を完全撤去(復活なし)。
理由: 復活させれば再びポリシー違反で制限される。ユーザー体験としても戻り操作を邪魔する広告は離脱要因になる。AdMob のポリシーページでも明記されている禁止事項。
2026-06-07: App Store 年齢制限は「疑似ギャンブル=頻繁/実ギャンブル=いいえ」で申告(→18+)
背景: Apple新方式の年齢制限アンケートで、パチスロ動画アプリをどう申告するか。「疑似ギャンブル」と「ギャンブル(実金)」の区別が肝。
決定: 賭博“コンテンツの閲覧”として 疑似ギャンブル=頻繁、賭博“を実際に行える機能”は無いので ギャンブル(実金)=いいえ。結果 18+。UGC=はい・広告=はい。次回以降の提出もこの方針を踏襲する。
理由: 実態に即した申告。ギャンブル(実金)を「はい」にすると実在賭博アプリ扱いとなりライセンス提出・地域制限・リジェクトを招くため絶対に避ける。18+は旧17+相当で妥当。
2026-06-07: iOSの広告は AdMob に集約する(nend/i-mobile を撤去)
背景: 自前で5広告網(AdMob/Nend/AdGeneration/Zucks/i-mobile)を束ねていたが、nend は2023年サービス終了、i-mobile の埋め込みframeworkは Xcode 26 でリンク不能。レビューでも収益≒ゼロの主因が死んだメディエーションと判明。
決定: nend と i-mobile を撤去し AdMob にフォールバック集約。AdGeneration / Zucks は現行Podで生存するため当面維持。将来は AdMob Mediation + AppLovin MAX 等への載せ替えを想定。
理由: 死網の維持はビルド不能要因かつ無収益。AdMob はメディエーション対応で現行サポートあり。AdGeneration/Zucks まで一度に剥がすとコード変更が膨らむため段階的に。
2026-06-07: iOS deployment target を 15 に引き上げる
背景: 旧構成は 9.0/11.0/11.2 混在。AdMob 13・Firebase 12・Realm 20 等は iOS 12〜15 以上が前提で、Xcode 26 の下限も 12。
決定: アプリ・拡張・Pod すべて iOS 15 に統一。
理由: 現行SDK群(特に AdMob 13 は iOS15必須)を通すための最小ライン。iOS15未満端末の切り捨ては避けられないが、広告SDK要件上不可避。
2026-06-07: OneSignal 通知拡張は一旦パススルー化(リッチ通知は後日再統合)
背景: OneSignal v5 で拡張は OneSignalExtension を要するが、use_frameworks!+拡張ターゲットで二重コピー競合・モジュール解決に難航。出荷を止めたくない。
決定: 通知拡張を OneSignal 非依存のパススルー実装にスタブ化(通常プッシュは動く、リッチ通知=画像付きは無効)。再統合は follow-up。
理由: リッチ通知は必須ではなく、出荷(=塩漬け脱却)を優先。基本プッシュは本体側の OneSignal v5 で機能する。
2026-06-07: 通知拡張の bundle ID を ...NotificationServiceExtension3 に変更
背景: App Store配信時、拡張ID ...NotificationServiceExtension2 が現Team(JFBRCYHN24)で登録不能("not available"=アカウント移管前の旧Team側が押さえたまま)。本体IDは移管済みで登録可。
決定: 拡張IDを未使用の ...NotificationServiceExtension3 に変更。
理由: 拡張はパススルー化済みでIDを変えても実害なし。新IDなら JFBRCYHN24 で登録でき配信が通る。旧IDの解放を待つより確実。
2026-05-04: v2 curate.py を v3 に統合する際は FastAPI ルーター化する
背景: v2 の curate.py は独立スクリプトとして cron で python curate.py 起動。v3 に移すにあたり、独立スクリプトのまま持ち込むか FastAPI ルーターにするか選択が必要。
決定: v3 の他の cron と統一して /cron/curate の FastAPI ルーターとして実装(cron/curator.py)。crontab からは curl http://127.0.0.1:8000/cron/curate で呼ぶ。
理由: v3 の他の cron(movieUploadsClassic / autoEntryMovie / updateHotMovies 等)がすべて HTTP エンドポイント方式で統一されているため、curate だけ独立スクリプトにすると運用とログ収集 (journalctl -u 7channel-web) が分散する。loopback URL なら Nginx/SSL のオーバーヘッドもなく、独立スクリプトと比べてコールドスタートも不要(既存 uvicorn プロセス内で実行)。
2026-05-04: 7channel は Lightsail Micro 1 台のみで完結させる
背景: v2 から v3 への吸収を進めるにあたり、多目的Lightsail にも一部役割を残すか、完全に Lightsail Micro 1 台に集約するかを判断する必要があった。
決定: 多目的Lightsail には 7channel 関連を一切残さず、Lightsail Micro 1 台に完結させる。多目的Lightsail は別プロジェクト(aix.ikapps.com / dev-timer / dev-tracker / mother / mydb.ikapps.com 等)専用。
理由: 7channel と他プロジェクトを混在させると、片方の作業時に依存関係を見落としやすい(実例: 今回の v2 DB_HOST が削除済み kizuna を指したまま 1 週間沈黙していた)。1 サービス = 1 サーバー の境界で運用する方が、移管・削除・ロールバック時の事故が減る。mydb.ikapps.com (phpmyadmin) だけは 7channel DB を覗くツールとして多目的Lightsail に残すが、それは「補助ツール」扱いで本体ロジックは無し。
2026-05-04: MySQL strict mode 互換は sql_mode 緩和ではなくコード修正で対応
背景: kizuna RDS → Lightsail MySQL 移行後、新DBはデフォルト strict mode(STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE)で、旧コードが暗黙の '0000-00-00' 投入や created カラム省略 INSERT で 500 連発。
決定: 短期止血の sql_mode 緩和ではなく、v3 の INSERT 文を strict 互換にコード修正する。'0000-00-00' フォールバックは '1970-01-01 00:00:00' に置換、必須カラム(created/updated/poplar_degree_updated)は明示。
理由: sql_mode 緩和は将来 MySQL アップグレードや別環境で再発する温床になる。コード修正は1セッションで終わる規模+永続的な改善になる。
2026-05-04: YouTube quota 拡張は別 Google Cloud プロジェクトでキー発行する
背景: movieUploadsDetail 用 YOUTUBE_API_KEY_2 が quota 超過。同じプロジェクト内でキーを増発しても 1日 10,000 ユニットの上限はプロジェクト単位で共有されるため意味なし。
決定: 新規 Google Cloud プロジェクトを作成 → そこで YouTube Data API v3 を Enable → キー発行 → YOUTUBE_API_KEY_3 として .env 登録。コード側は KEY_3 を参照。
理由: quota はプロジェクト単位(10,000/日)。プロジェクト分離が唯一の正攻法。同時に「API レスポンスが 200 以外 / error キーありなら del を変更せずスキップ」という防御もコードに入れて、quota 超過時に「全動画削除済み」誤判定で連鎖障害を起こさないようにする。
2026-05-04: ローカルレガシー PHP テスト用 DB は MariaDB を採用
背景: macOS Docker 上のレガシー CakePHP(PHP 5.x 時代の mysqlnd 同梱)から Lightsail MySQL 8 へ接続すると [2054] Server sent charset unknown to the client(collation ID 255 を古い PHP が認識できない)。
決定: ローカル MySQL は MariaDB(brew install mariadb)にする。Lightsail からはスキーマのみ(mysqldump --no-data)取得し、データは空でローカル流し込み。
理由: MariaDB は collation がレガシー互換でハマらない。MySQL 8 + collation 設定上書きでも動かせるが、設定漏れリスクがある。スキーマのみ持って来る方針はローカルが本番に影響しない+手間最小。
2026-05-04: Lightsail OS と MySQL を JST 化、過去データには触れない
背景: Lightsail はデフォルト UTC で動作し、NOW() も publishedAt 由来の upload_date も UTC で記録されていた。旧 PHP 本番は JST 運用だった。
決定: timedatectl set-timezone Asia/Tokyo + MySQL 再起動で system_time_zone=JST に。コード側で YouTube publishedAt を astimezone(ZoneInfo("Asia/Tokyo")) で JST 変換してから格納。過去データ(既に UTC で入った行)は書き換えない。
理由: JST 運用に揃える方が UI 表示・運用判断と一致して自然。過去データ書き換えは差分が大きく、JST 化のメリットに見合わない。今後の INSERT/UPDATE のみ JST にすれば実用上問題ない。
2026-05-04: Lightsail Nano では MySQL 同居不可、Micro ($7/月) にアップサイズ採用
背景: 段階3最終で kizuna RDS を Lightsail に移行する際、Nano (512MB RAM) にFastAPI+MySQL同居は OOM 多発で実質不可能。1回目 import 中にサーバー応答停止 → 強制 reboot 経験。
決定: Lightsail を Nano $5 → Micro $7 (1GB RAM) にアップサイズ + swap 1GB追加。MySQL の innodb_buffer_pool_size = 128M などチューニング。
理由: ① +$2/月で安定性が圧倒的に向上、② 550MB DB に対して 1GB RAM は妥当な比率、③ swap は import 時のスパイクに備える保険、④ アップサイズはスナップショット → 新インスタンス → 静的IP切替で実質ダウンタイムなし。
2026-05-04: CloudFront を廃止し Let's Encrypt + Nginx 直接 HTTPS に切替
背景: CloudFrontはCachingDisabled運用で実質「ACM証明書を貼るためだけ」の存在になっていた。最終ゴールの「Lightsail 1台集約」を達成するため、HTTPS終端も Lightsail で完結させたい。
決定: Lightsail に certbot + DNS-01 challenge (Route53) で Let's Encrypt 取得、Nginx で 443 SSL 終端。Route53 7channel.ikapps.com を CloudFront ALIAS → Lightsail 静的IP に切替。CloudFront は Disable して月末以降に削除予定。
理由: ① 構成が1段シンプルに(iPhone → Lightsail 直接)、② レイテンシ改善(CloudFront経由 90ms → 直接 60ms)、③ ACM証明書管理から解放、④ Let's Encrypt は完全自動更新(90日毎)、⑤ DAU 500・日本のみのトラフィックで CloudFront のエッジ分散は過剰。
2026-05-04: B-6 (beginBroadcast/S3Uploader) は保留不可、iOSアプリのデータ供給に必須
背景: 段階3で B-6 (/api2/beginBroadcast) を「cron 10分毎で影響低」と判断して保留していたが、cutover後に iOSアプリ "Avatar3" + cron の双方が叩いて 404 を出していた。元 PHP 実装を読み直したところ、/S3Uploader/current201609 で全テーブルを集計して S3 (channel7/current/201609) に public-read で put し、iOSアプリは S3 から直接基本データ(top_movie / cast / channel / device 等)を取得する設計だった。
決定: B-6 を緊急で v3 に実装(cron/s3_uploader.py、22テーブル集計→ boto3 で S3 put)。Lightsail に IAM ユーザー認証情報を ~/.aws/credentials で配置。
理由: ① iOSアプリの基本データ取得経路を放置できない(古いデータ表示・新着動画が出ない)、② cron 10分毎の更新が必要、③ S3 への public-read アクセスは既存運用の継続。
教訓: 「保留してもいい」と判断する前に、そのエンドポイントが何のために存在するかを実装ベースで深掘りすべきだった。
2026-05-03: Lightsail → kizuna RDS 接続は「Public化 + SG許可」方式(VPC peering 不可のため)
背景: Phase D で Lightsail から kizuna RDS にアクセスする経路が必要。schulte-operation の前例(VPC peering)を検証したが、Lightsail VPC peering は「default VPC とのみ可能」という公式制約があり、kizuna はカスタムVPC vpc-4b77802e にいるため使えない。選択肢は ① EC2-2 を踏み台にした SSH トンネル、② RDS を Public化して SG で Lightsail static IP を許可、③ 一気に Lightsail MySQL に移行、の3つ。
決定: ②(RDS PubliclyAccessible=true + SG 35.73.116.227/32 許可)を採用。kizuna は前は Public だったので元に戻すだけ。
理由: ① はSSHトンネル維持の追加運用負担(autossh等)が発生・EC2-2を踏み台専用で残す必要、③ は段階3の本流だが今すぐは規模が大きくスケジュール圧迫。② は最小工数で確実に動き、最終段で RDS も Lightsail に持っていく時に簡単にロールバックできる。Public化のセキュリティ後退は IP制限とパスワード認証で許容範囲。
2026-05-03: 段階3 はPHP→Python(FastAPI)移植を v3 新規実装で進める(v2 を吸収)
背景: 段階3で「Lightsail 1台に集約」を目指すが、PHP 5.3 + 旧CakePHP は新環境に持っていけない。多目的サーバーは既に v2 + AI スクリプトが居て、レガシーPHPを混ぜたくない。schulte-operation の前例(PHP→FastAPI で Lightsail 移行・Weighted Routing カットオーバー)を参考に方針確定。
決定: 7channel-server-v3 を新規作成(v2をベースにコピーし v2 admin機能をそのまま吸収)。iOSアプリ用 API(access log 分析で特定した 9 メソッド)と cron 系(12メソッド)を Python/FastAPI で再実装。最終的に Lightsail Small ($10/月) にデプロイして CloudFront のオリジンを切替する。
理由: ① PHP 5.3 互換性問題を完全回避できる、② iOS app が使ってるエンドポイントは access log で実証済み(9個のみ)、PHP 60+ Controller のうち本当に必要なのはごく一部、③ Python なら既存 v2 と運用ノウハウを共有できる、④ schulte の前例で同じパターンの成功実績あり、⑤ Lightsail 集約で月-$50(年-$600)の削減見込み。
2026-05-03: ローカル開発時の RDS 接続は SSH トンネル経由(kizuna は PubliclyAccessible=false のため)
背景: kizuna RDS は PubliclyAccessible=false のためローカル直接接続不可。v3 ローカル開発で本番DBにアクセスして PHP本番 vs FastAPI 比較テストをしたい。
決定: EC2-2 (43.206.39.55) を踏み台にして SSH ローカルポート転送(-L 13306:kizuna....:3306)でアクセス。v3 の database.py に DB_PORT パラメータを追加して、環境変数で 13306 に切替可能にした。
理由: ① PubliclyAccessible を一時 ON するよりセキュアで副作用なし、② RDS の SG変更も不要、③ 終了時に SSH を閉じるだけでクリーンアップ完了、④ EC2-2 は同じ VPC 内なので RDS にアクセス可能。
2026-04-28: STRAIGHT_JOIN は本番投入せず、ロールバック後はインデックス調整のみで対応
背景: CastMovieRelation::getRows を STRAIGHT_JOIN 化したところ、EXPLAINテストでは250msだったが本番では最大14秒・平均7.7秒まで悪化。データ分布が偏ると movies 駆動でLIMIT 30を満たすために大量スキャンが発生する典型パターン。
決定: STRAIGHT_JOIN を削除して元の自然JOIN形式に戻す(ロールバック)。今後 JOIN順序の固定(STRAIGHT_JOIN)は本番投入しない。スロークエリ対策はインデックス追加とアプリレベルの単純な書き換え(COUNT化、ORDER BY 列の置換等)のみに限定。
理由: ① EXPLAINや1ケースのテストではデータ分布の偏りを検出できない、② オプティマイザを縛る変更は劣化リスクが大きい、③ インデックス追加だけでも同等以上の効果が出ることが多い(過去に証明済み)。
2026-04-28: 7channel本番のHTTPS終端は CloudFront 経由(Let's EncryptでなくACM流用)
背景: 段階2でCLB廃止する際、CLBがACM証明書でHTTPS終端していたため代替が必要。当初はEC2-2にLet's Encrypt導入(A案)の方針だったが、Amazon Linux AMI 2018.03 がEOLでcertbot導入が困難と判明。
決定: CloudFront を間に挟む(B案)に変更。新たに us-east-1 でACM証明書を発行し、CloudFront に紐付け。CloudFront → EC2-2 はHTTP(80)で内部通信。多目的サーバー集約(最終ゴール)は段階3以降に持ち越し。
理由: ① EC2-2 のEOL OS で証明書管理を増やすのは技術的負債、② CloudFront は月$0.5程度の追加コスト(DAU 500)で運用が安定、③ ACM証明書の自動更新がAWS任せで運用ゼロ、④ EOL Apache を表に出さなくて済むセキュリティ面のメリット。
2026-04-28: EC2-2 は専用機として残し、多目的サーバーには7channelを置かない
背景: 段階2の当初プランは「多目的サーバーNginxに集約」だったが、ユーザーから「多目的サーバーには既に v2 + AI スクリプトがあり、レガシーPHPを混ぜたくない」との方針確認。
決定: EC2-2はそのまま専用機として残す(CLBだけ廃止して、CloudFront → EC2-2 直接)。最終ゴールの「Lightsail 1台集約」は、PHP 5.3 + 旧CakePHPのリプレースが終わってから実現する(段階3以降)。
理由: ① レガシーアプリと新アプリの相互影響を避ける、② 多目的サーバー(Nano $5)にPHP本番アプリの負荷を載せるのは性能的にもリスク、③ PHP 5.3 は新OSに移植が困難で、Lightsail集約はリプレースとセット運用が現実的。
2026-04-27: CakePHP find/getAll 経由のクエリはコード書き換えせずインデックスで対症療法
背景: Api3Controller::nextMovieDataForCast のクエリ(A: 950ms)が MovieCastRelation->getAll(...) 経由で recursive=1 の自動JOINになっており、ORDER BY が JOIN先の列のため filesort が発生。インデックスで270msまで改善するが、根本解決は2段階クエリ化や recursive=-1 + 手動 joins への書き換えが必要。
決定: A はインデックス追加(movies.idx_del_upload_unix)のみで対応し、コードは書き換えない。一方、直接 SQL を組んでいる Model メソッド(CastMovieRelation::getRows 等)は STRAIGHT_JOIN への書き換えを行う。
理由: ① CakePHP find/getAll の戻り値は $row['Movie'][...] 形式で、JOIN方法を変えると配列構造が変わりやすく、API契約に影響するリスク。② 270ms はスロー閾値1秒未満で実用上OK。③ 直接 SQL の Model メソッドはレスポンス形式が固定なので STRAIGHT_JOIN 書き換えが安全かつ効果的。
2026-04-27: cast_imps は書き捨てテーブルとしてINSERTを停止、データもTRUNCATE候補
背景: 540万行・250MB のテーブルだが、ソースコード grep で SELECT 箇所がゼロと判明。INSERT のみされ続けて読まれていない死テーブル。
決定: Api3Controller::impressionCast() の INSERT 行をコメントアウトしてデプロイ。テーブル本体のTRUNCATEは保留(ユーザーが「とりあえずいいや」と判断)。
理由: 完全に未使用と確認できたため、書き込み停止に副作用なし。実テーブルは後でいつでも消せるので、安全マージンとして残す。
2026-04-27: movie_imps は直近3ヶ月のみ保持、旧データは old_movie_imps に退避
背景: 1100万行・1.5GBの閲覧履歴ログ。HotMovie/HotWeekMovie/HotMonthMovie/HotTotalMovie のランキング集計用。/3//4 cron は休止中だったが、/3(1ヶ月)は復活希望。
決定: テーブルswap方式で直近3ヶ月(約2万行)に圧縮。旧データは old_movie_imps に退避してLightsail移行時までキープ。/4(10年集計)は完全データがないため休止継続。さらに cleanMovieImps cron を追加して3ヶ月超を毎日自動削除。
理由: ① INSERT/SELECT継続、② 復活させた /3 は3ヶ月分で十分カバー、③ swap方式はDELETEより高速・安全、④ 自動クリーンアップで再肥大を防止、⑤ 旧データは消さずに残すことで万一の検証に対応。
2026-04-27: スロークエリ対策はインデックス追加とアプリ側クエリ改善の両輪
背景: スロークエリログ有効化後、ワースト3クエリを特定。articles.done_image_scraping(インデックス無し・31万行スキャン)、updateCastMovieCount(1628行fetchしてPHP count())、ORDER BY DATEDIFF(毎行計算)。
決定: ① インデックス追加(articles.done_image_scraping)、② SQL COUNT(*) への置換(find('count', recursive=-1))、③ DATEDIFF(定数, col) ASC を等価変換で col DESC に書き換え(3箇所)。
理由: インデックスはノーリスクで効果大。クエリ改善は CakePHPの recursive パラメータと数学的等価性を活用すれば挙動変更ゼロ。両輪でアプローチすれば本番影響を抑えつつ最大効果。
2026-04-26: AWSコスト削減は超段階的にやる方針を採用
背景: 7channel本番環境(EC2×2 + CLB + RDS、月$106)は技術的負債(PHP 5.3 / Amazon Linux 2018.03 / nodejs8.10 Lambda 等の全EOL)も抱えており、一気に作り直すと事故リスクが高い。
決定: リプレースではなく段階的なコスト削減で進める。最初の段階1として、ALB配下のEC2を2台→1台に減らす(片方の AppName タグを外して CMN_attachToELB の自動再登録を回避し、CLBからデタッチ+停止)。観察期間を経て段階2以降へ。
理由: ① ロールバックが1コマンド(タグ戻すだけ)で可能、② サービス影響ほぼゼロ、③ 1台で本当に捌けるかの実測データが取れる。「いきなりLightsail移行」よりリスクが極小。
2026-04-25: Lightsail 上のアプリから AWS リソースへのアクセスは IAM ユーザーのアクセスキー方式を採用
背景: SNS アラートメールが新サーバーから飛ばなくなった。Lightsail のデフォルトロール AmazonLightsailInstanceRole には SNS:Publish 権限がなく、ロール自体が AWS 管理リソースのため IAM ポリシーをアタッチできない。SNSトピック側にクロスアカウント許可を入れても、IAM側ロールに権限がないため動かない。
決定: IAM ユーザー ichirokisanuki のアクセスキーを ~/.aws/credentials(mode 600)に配置して、boto3 のデフォルト認証チェーン経由で SNS を叩く方式にした。
理由: Lightsail のデフォルトインスタンスロールは AWS 管理で変更不可なので、ロールベース認証は実質不可能。アクセスキー方式は旧サーバーでも採用していた実績ある方法で、運用がシンプル。
2026-04-24: 7channel-server-v2 を独立リポジトリではなく7channel-operationに統合
背景: 7channel-server-v2 は元々独立したGitリポジトリ(.git 内蔵)として運用していたが、7channel-operation モノレポからもgit管理したかった。サブモジュール化と統合の二択だった。
決定: 7channel-server-v2/.git を削除して 7channel-operation リポジトリに通常のディレクトリとして統合。GitHub上の独立リポジトリは作られていなかったので破棄も不要。
理由: 個人開発でサブモジュールを使うほどの複雑さは不要。1リポジトリで完結する方が運用が楽。
2026-04-24: 7channel-server-v2 を多目的Lightsailサーバーに移行
背景: 旧Lightsailインスタンス(ec2-user@13.230.63.19)で動いていた7channel-server-v2を、新しい多目的サーバーに集約したかった。
決定: multipurpose-server1(ubuntu@13.230.63.19)に移行し、Nginxバーチャルホストで 7ch.ikapps.com をルーティングする構成にした。
理由: 複数サービスを1台でNginxで束ねる運用に統一するため。旧インスタンスを廃止してコストを抑える意図もある。
2026-04-24: 静的IP 7channel-v2-ip を多目的サーバーにアタッチして固定運用
背景: 多目的サーバーは動的IPのままだと再起動のたびにRoute53を更新する必要があり運用が煩雑。
決定: 旧サーバーで使っていた静的IP 7channel-v2-ip(13.230.63.19)を多目的サーバーに移植してアタッチ。
理由: Lightsailの静的IPはアタッチ中無料。Route53のAレコードを固定できるため運用が楽になる。
DEVLOG(作業ログ)
開発日誌
このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。
2026-06-18
06:00 - 記事日付の1970/01/01バグ修正+記事一覧の画像なしレイアウト改善+iOS 8.3.2 審査提出
iOSアプリの記事一覧で日付が 1970/01/01 になる記事が大量に上位表示される不具合を調査・修正し、あわせて画像なし記事のレイアウト改善を行い、8.3.2 を App Store 審査に提出した。
やったこと:
- 日付1970/01/01バグの原因特定: DB(articles.date)は正常で、原因は API のシリアライズ形式と iOS のパース形式の不一致だった。API(
api/common.py serialize_dates)がisoformat()で2026-06-18T...(Tセパレータ+ハイフン)を返す一方、iOS(PrimitiveObject.swiftのinit(json:)/UtilExtension.stringToDate)はyyyy/MM/dd HH:mm:ss(スラッシュ+スペース)しか受け付けず、パース失敗→nil→Realm既定値のエポック(1970/01/01)に落ちていた。 - サーバ修正(本番反映済み):
serialize_datesを%Y/%m/%d %H:%M:%S形式へ変更(date型は%Y/%m/%d 00:00:00)。これで movies の upload_date も含め全 date を iOS 期待形式で配信。あわせてcron/article_crawl.pyの INSERT フォールバックを"1970-01-01 00:00:00"→datetime.now(timezone.utc)に変更し、RSSに日付が無い記事が今後エポックで入るのも防止。35.73.116.227 へ scp+7channel-web再起動し、/api3/nextArticleDataが2026/06/17 20:30:41形式で返ることを検証。この修正はサーバ側のみで全ユーザーに即時反映(アプリ更新不要)。 - 記事一覧の画像なしレイアウト改善(iOS): 画像が無い記事はサムネイル領域(66pt)を畳んでタイトルを右端まで広げるようにした。
ArticleN1Cell.xibの imageView 幅制約(oDk-gs-J5N, 66)に新outletthumbnailWidthConstraintを接続し、OnePieceArticleCellにアクセサ追加、CoreArticleCell.putArticleで画像なし時に幅0+isHidden=true。タイトルの trailing が imageView.leading に固定されているため自動で拡大。ios-test(iPhone 17 / iOS 26.5)でビルド成功・エラー0を確認。 - 8.3.2 審査提出: 上記2件+前回(8.3.1)以降の蓄積(視聴履歴タブ追加・番組0件チャンネルのタップ直行・YouTube埋め込み自動再生オフ・try?化のクラッシュ対策 ほか)を含めて 8.3.2 を審査提出。リリースノートは「・視聴履歴機能を追加 / ・アプリの安定性を向上しました」。
詰まったこと・気づき:
- 最初DBのエポックレコードを疑ったが0件。
isoformat()のT/ハイフンと iOS 既存パーサのyyyy/MM/dd不一致が真因で、v3移植時にPHP版のレスポンス形式(スラッシュ区切り)を踏襲できていなかったのが根。 - 7channel-ios は CodeCommit(master) 管理=push は
AWS_PROFILE=ichirokisanuki git push origin master。サーバ(7channel-operation)は GitHub(main)。2リポジトリにまたがるセッション。
コミット: server=4e91d39(GitHub main) / iOS=a398979(CodeCommit master)。
2026-06-16
12:17 - 出演者関連付けの自動化(Codex判定)と movie_cast_relations へのテーブル一本化
動画への出演者(cast)の関連付けを Codex CLI で自動化し、あわせて二重持ちだった関連テーブルを一本化した。
やったこと:
- cast_matcher.py 新規: 出演者マスタ(casts: 名前/別名 other_matching_name/除外 no_matching_name)+動画タイトル・概要を
codex exec(非対話・--output-schemaでJSON強制・read-only)に渡し、出演者IDを判定→movie_cast_relationsへ書き込むバッチ。dry-run(8/30件)で精度確認し、概要末尾の「配信スケジュール/番組一覧/おすすめ」の他人名を拾う誤検出をプロンプトのルール追加で解消。 - 定期実行(launchd):
run_cast_matcher.sh(13308トンネル自動確認+多重起動防止)を~/Library/LaunchAgents/com.7channel.cast-matcher.plist(StartInterval 1800=30分)で回す。asc-user-trend のレシピ踏襲。Codex認証がMac側のみなのでMacで実行。 - テーブル一本化(段1〜3・本番反映):
cast_movie_relations(cmr, youtube_code基準)とmovie_cast_relations(mcr, movie_id基準)は同じmovie↔cast情報の二重持ち。mcrを正に統一。①readers(api/common.py attach_casts, cron/s3_uploader.py)をmcr読みに ②mcrの重複239グループ247行を削除しUNIQUE(movie_id,cast_id)付与(バックアップmcr_dedup_backup_*.json) ③writers(youtube_uploads autoEntry, database.py, main.py admin, cast_matcher)をmcr直書きに+relation_syncルート/crontab行を退役。本番API nextMovieData で casts 正常配信を検証。 - watermark方式: 処理済み最大movie.idを
cast_matcher_state.json(Mac local/gitignore)で持ち、id>watermarkを古い順に1回ずつ判定→前進。(なし)動画も前進対象で再判定ループ無し。初回は現在の最大idで初期化しバックログ無視。 - (a)取り込み時固定付与を撤去: youtube_uploads autoEntry の番組ルール固定キャスト付与を削除。新着は出演者ゼロで着地→cast_matcher(Codex)が唯一の決定者に。
- 判定知見ファイル:
knowledge/cast_matching_hints.md(人間がメンテ)を cast_matcher がプロンプトへ毎回注入。誤判定を見つけたら1行追記=実質の学習。今セッションの知見(成り上がり人生録=よしき単独/台タイアップのグラビア名/配信スケジュール表記)を初期投入。
決めたこと: (DECISIONS.md 参照)出演者判定にCodex CLIを採用 / mcrを正にテーブル一本化 / cast_matcherはcast_program_relationsを見ない / 判別はwatermark方式(per-movieフラグ不採用) / 取り込み時固定付与を撤去しMac依存を許容。
次回やること: サーバー側Codex化(取り込み直後リアルタイム発火でMac依存解消) / cast_movie_relations の DROP(観察後・段4) / knowledge ファイルの育成。コミット: c9d5a08, 426467c, 本まとめ分。
is_single_program チャンネルの同名番組を撤廃(動画詳細の冗長表示を解消)
is_single_program(チャンネル全体を1番組扱い)でチャンネルを作ると、チャンネル名と同名の番組が自動生成され、新規動画にその番組が割り当たっていた。結果 iOS の動画詳細でチャンネル名と番組名が同じ文字列で縦に並び冗長だった(例: スロぱちの自由席 / まんぱつ 等)。これを撤廃した。
やったこと:
- サーバ(main.py):
channel_createから同名番組の自動生成ブロックを削除。今後 is_single_program で作っても番組は作らず、動画はprogram_id=0のまま。curate のget_single_program_for_channelも番組が無ければ 0 を返すので整合。本番(35.73.116.227)へ scp +7channel-web再起動済み - 本番データ補正: is_single_program かつ「チャンネル名と同名の番組」を持つチャンネル全 21件(直近追加16+レガシー2021年5件)の同名番組を
del=1、紐づく動画 計 3,688本 をprogram_id=0に戻した。各単一番組チャンネルは生きてる番組がちょうど1個で曖昧さなし。id93 オワコンちゃんねる(番組名≠チャンネル名)は対象外 - iOS(ChannelCell.swift):
tappedを改修。番組0件のチャンネルをタップしたら空の番組一覧(ProgramListByChannelViewController)でなく、直接チャンネル動画一覧(MovieListViewController + channelID)へ遷移。これで「番組タブ→チャンネル→空画面」の行き止まりを解消 - 検証: ios-test(iPhone 17 / iOS 26.5)でビルド成功・エラー0、起動クラッシュなし。0番組チャンネル「だいいち!」タップ→チャンネル動画一覧へ直行を確認。S3マスターが既にサーバ変更を反映し冗長表示が消えていることも確認
- 反映経路: アプリは起動時に S3 マスター(
/api2/beginBroadcastが10分毎再生成・p.del=0絞り)を取得し全 Realm を入れ替えるため、既存ユーザーも次回起動で冗長表示が消える
別件メモ(今回対象外・既存データ問題):
del=1の番組を指したままの生存動画が 1,563本(program1 閉店くんが行く=1265 / program132 打ってけ!TV=298)。消滅疑いチャンネルの棚卸しと併せて別途判断
2026-06-13
07:08 - チャンネル大量追加の運用開始+検索改善+画像自動修復+admin認証
前日に作ったチャンネル管理を実運用に投入し、出てきた課題を片っ端から潰したセッション。最後に admin のセキュリティ問題を発見して保護した。
やったこと:
① チャンネル候補の探索 → 大量追加(コンテンツ拡充)
- 本番の YouTube API キーでサーバー上から8キーワード検索(search.list type=channel)→ 既知104除外で未登録候補102件を発見
- 桜鷹虎(79.9万)・あすピヨ(61.6万)・やっちゃんの崖っぷち(35.1万)・スロぱちの自由席(隣駅の姉妹)・寺井一択・ガリぞう・コンコルドちゃんねる等を提示 → ユーザーがほぼ全部追加
- 結果: 稼働121チャンネル、movies は1日で+16,749本。バックログ0で全パイプライン健康確認済み
② 番組編集の動画検索をチャンネル内に限定(d427b93)
- 「過去の動画に番組を割り当て」が全 movies 横断検索だったのを、番組の channel_id 内に限定。channel_id=0 の番組のみ従来どおり全体検索
③ 過去動画の遡り上限 2000本→1万本(5694aed)
- 2000は自分で付けた安全上限で API 制限ではない。httpx.Client の接続使い回しで高速化(実測: 5,071本を37秒)
- Nginx に /admin location を追加し proxy_read_timeout 300s(iOS API 側は60sのまま)
④ 取り込みボタンを結果リストの上にも追加(12e281f)
⑤ 管理画面の動画検索を FULLTEXT → LIKE に変更(138a7a2)
- 「ミリオン★タッグ」検索で無関係の動画(ミリオンゴッド/最強タッグ等)がヒット。原因は★でFULLTEXTトークンが割れて BOOLEAN MODE が OR 検索になること
- フレーズ化("...")も記号位置ズレで0件になるため LIKE 部分一致に変更(実測: 204件正確ヒット・96ms、全体検索でも0.4秒)
⑥ チャンネルアイコンのリンク切れ自動修復 cron(dbb1670)
- チャンネル側がアバター変更すると旧 yt3.ggpht.com URL が無効になる問題
/cron/refreshChannelImages新設(channels.list 50件/1unit でほぼ無料)、crontab 4:15 JST 日次- 初回実行: 121件中99件の古いURLを一括修復。missing 3件=チャンネル消滅疑い(打ってけ!TV[動画500本]・D'STATION TV[9本]・キクヤチャンネル[70本])→ 棚卸し対象
⑦ admin のセキュリティ保護(サーバー側のみ・コミットなし)
- 「/admin って誰でも見れる?」→ 完全公開状態だった(認証ゼロ、チャンネル/番組のCRUDが誰でも実行可能。cron系も公開で quota 浪費攻撃が可能だった)
- Nginx の /admin に Basic 認証(ユーザー名
7ch、htpasswd は/etc/nginx/.htpasswd-admin、パスワードはパスワードマネージャー保管) - cron専用エンドポイント(/cron・/crawl・/api3/updateHotMovies・/api2/beginBroadcast)を外部から403遮断
- crontab を全部
http://127.0.0.1:8000直叩きに書き換え(Nginx非経由) - 検証: 認証なし401/あり200、cron系403、iOS API は200のまま無影響、localhost cron 200
気づき:
- Nginx 設定のバックアップは
/etc/nginx/sites-available/7channel.conf.bak-20260612 - FULLTEXT(ngram) は記号入り日本語に弱い。管理画面用途は LIKE が正解(180k行でも0.4秒)
次回やること:
- AdMob 制限解除の審査結果待ち(継続)
- 消滅疑い3チャンネル(id 12/36/98)の棚卸し(動画ごと論理削除するか判断)
- 大量追加後の様子見: curate 採用通知のノイズ、program_id=0 動画の量、アプリの見え方
2026-06-12
20:43 - admin にチャンネル管理+YouTube過去動画取り込みを実装(動画を簡単に増やせる仕組み)
「アプリで扱うチャンネルを簡単に増やしたい」に対応。v3 admin に2つの機能を新設し、本番デプロイ・実データで動作確認まで完了。
やったこと:
① チャンネル管理ページ(/admin/channels、コミット 30fd36e)
- 仕組みの現状把握:
channelsに1行足せば RSS巡回 → movie_uploads → curate → movies の既存パイプラインに全自動で乗る。ボトルネックは「チャンネル追加の入口がない」(phpMyAdmin手作業)ことだった - 追加フォーム: YouTube の URL / @ハンドル / チャンネルID / 動画URL のどれを貼っても YouTube API(
channels?forHandle=等)で channel_code・名前・アイコン・登録者数を自動解決してプレビュー → 確認して登録 - 登録済みチャンネルは警告+編集リンク表示(重複防止)。論理削除済みなら復活
is_single_programON なら同名番組も自動作成(動画の受け皿を同時に用意)- 登録時
crawled_date='2000-01-01'にして次回 cron で最優先クロール - 一覧: 動画数・最終クロール・優先クロールボタン・論理削除。ナビ/トップメニューにも追加
② チェックボックス描画バグ修正(6daaae2)
- base.html の
.form-group inputへのappearance: none一括指定がチェックボックスの描画を消していた(実際はONなのに見た目は常に空白、タップしても見た目が変わらない) - ユーザーの「チェックしてないのに番組ができた」の正体はこれ。
input[type="checkbox"]だけ標準描画に戻して解決
③ YouTube過去動画の検索・取り込み(チャンネル編集画面、7e9311d)
- RSS は直近15本しか取れないため、過去動画はチャンネルの uploads プレイリスト(UC→UU変換)を遡って取得
- quota の高い search.list(100units)ではなく playlistItems(1unit/50本)を採用。最大2000本まで遡れる
- タイトルでチャンネル内検索 → 登録済みはグレー表示 → チェックして取り込み →
program_id=0で movies に登録 - 番組割り当ては番組編集画面の既存「過去の動画に番組を割り当て」機能で行う2段構え(当初は番組画面に直接取り込みを作りかけたが、ユーザー指示でチャンネル画面+program_id=0方式に変更)
- 実測: スロぱちの隣駅で「スロパチTV」検索 → 229件ヒット、RSS済み分は登録済み検知OK
④ 新着フィード占拠バグ修正(51feb9e)
- 新規チャンネル追加直後、RSSで拾った過去動画15本が
upload_unixtime=現在時刻で登録されアプリのトップを占拠 curate/autoEntryMovieの両方を実投稿日時(movie_uploads.upload_date、JST)由来の unixtime に修正- 既存データはスロぱちの隣駅(channel 112)の15本を movie_uploads と JOIN して DB 直接修正
- 手動取り込み(③)側は最初から実投稿日時を使う設計にしてあった
運用メモ:
- チャンネル「スロぱちの隣駅」(id=112) 追加済み。ユーザー判断で
is_single_program=0に変更、同名番組(id=584)は論理削除し、番組はユーザーが手動で作成(スロパチTV等で分ける運用) - デプロイ方法:
scp -i ~/.ssh/lightsail-7channel.pemで 35.73.116.227 の~/7channel-server-v3/へ変更ファイルのみ転送 →sudo systemctl restart 7channel-web(deploy.sh は旧サーバー向けで使えない・要更新)
次回やること:
- AdMob 制限解除の審査結果待ち(継続)
- チャンネル追加・過去動画取り込みの実運用フィードバック対応
- 取りこぼし確認ビュー(program_id=0 の動画一覧)やチャンネル発見支援は「いつか」ストック
18:07 - 8.3.1 リリース完了 → AdMob 制限解除の審査リクエスト提出
やったこと:
- 8.3.1 が Apple 審査を通過し App Store 公開完了(前日提出 → 翌日承認のスピード審査)
- App Store Connect のリリースノート文面を作成: 「動画詳細のYouTube動画を自動再生しないように変更(タップで再生開始)・動作の安定性向上・広告の仕組みを更新」
- AdMob ポリシーセンターから制限解除の審査リクエストを提出
- 対象問題: 「サイトの動作: ナビゲーション」(戻り操作時のインタースティシャル)
- 詳細欄の文面を作成して提出: ①違反内容の理解(ナビゲーション操作直後のインタースティシャル表示)、②修正内容(インタースティシャルをコードごと完全削除)、③修正版 8.3.1 が App Store 公開済みであること、の3点構成
- AdMob の審査は通常1週間程度
次回やること:
- AdMob 審査結果待ち(〜1週間)。解除されたら rate 見直しを相談(ネイティブ/バナーを AdMob 優先に戻すか。レクタングルは AdMob 禁止枠なので Zucks のまま)
- Zucks バナー枠
_50d0640137の配信開始確認(実トラフィックでの fill 確認) - 8.3.1 本番の実データ確認: ATT 許諾率、Crashlytics クラッシュ、Zucks の収益数値
2026-06-11
iOS 8.3.1: 広告系全面リワーク+YouTube自動再生オフ
8.3.0(2026-06-09リリース)の翌セッションで、広告クラッシュ・ポリシー違反・収益改善をまとめて実施。次回アップデート 8.3.1 の実装を全完了。
やったこと:
① AdMob XIBクラス名修正(クラッシュ修正)
GADUnifiedNativeAdView→GADNativeAdView(SDK v13 でリネーム、旧名でクラッシュ)- 対象:
AdmobNativeAdvanceContentBigMovieNativeAdCell.xib/AdmobNativeAdvanceContentBigMovieNativeAdView.xib
② スクロールクラッシュ修正
AppDelegate.viewControllergetter でtopViewController→viewControllers.first- 動画詳細画面 push 中にスクロールすると
swift_dynamicCastFailureでクラッシュしていた
③ AdMobポリシー違反の除去(インタースティシャル廃止)
MovieDetailViewControllerから戻り操作時のインタースティシャル表示を完全撤去- AdMob コンソールが「制限付きで広告配信中」「サイトの動作: ナビゲーション」→根治が必要だった
InterstitialAd.shared.show()/ timer /INTERSTITIAL_AD_THRESHOLD変数すべて削除
④ SKAdNetworkItems 50エントリ追加
Info.plistに Google公式リスト 50エントリ追加(iOS 14+ 帰属計測対応)
⑤ ATT許可プロンプト実装
AppDelegate.applicationDidBecomeActiveでATTrackingManager.requestTrackingAuthorization(0.8秒遅延)- 実機で表示確認済み。Authorized状態でeCPM 1.5〜3倍の改善期待
⑥ Crashlytics dSYM自動アップロード設定
- Xcode Build Phase の run script を
"${PODS_ROOT}/FirebaseCrashlytics/run"に差し替え - inputPaths(dSYM / GoogleService-Info.plist / executable)を正式指定
⑦ Zucks直接化(バナー・ネイティブ・レクタングル)
- AdMob が「制限付きで配信中」(全枠 "No ad to show")のためZucksを直接配信に切り替え
- バナー:
AdBannerViewがデフォルトでZucksBanner(枠_50d0640137)を呼ぶ - ネイティブ:
ZucksBigMovieNativeAd(ZADNNativeAdLoader、枠_cc69bfb840)を新規実装、MovieNendBigMovieNativeAdCellを転用表示セルに。実機確認済み(「完璧よ」) - レクタングル(動画詳細): DBのrate更新(id7=directNend=0, id8=Zucks=1000)のみ。コード変更なし。実機確認済み
- サーバーDB(
ad_banner_rates/ad_native_for_big_movie_rates/ad_rectangle_rates)を全面棚卸し → Zucks 1000 / 死活ネット 0 に更新・S3再生成
⑧ YouTube動画詳細を自動再生オフ
YoutubePlayerView.swift:autoplay=1→autoplay=0- コミット(iOSリポ master):
03ccdac「動画詳細のYouTube埋め込みを自動再生オフに」
⑨ バージョン 8.3.1 へバンプ
CURRENT_PROJECT_VERSIONを8.3.0.1→8.3.1に統一(通知拡張設定も同期)
詰まったこと / 気づき:
- AdMob "No ad to show" の根本原因はポリシー違反(インタースティシャル on back)。コンソールの「制限付きで広告配信中」+「サイトの動作: ナビゲーション」がヒント
- Zucks SDK の
delegateはweak参照なのでZucksBanner()をインラインで生成すると即解放 → 保持用フィールドが必要 - Zucks 新枠
_50d0640137は errorType=4 → SDK動作は正常、需要割り当て待ち - DAU は GA4上の数字が MAU と混同されていた。実際の DAU は ~500(MAU が 7,000〜9,000 水準)
次回やること:
- 8.3.1 を Archive → App Store 提出(You の作業)
- 提出後: AdMob コンソール「審査プロセスを開始」クリック
- Zucksバナー枠
_50d0640137の配信開始確認(数時間〜翌日) - AdMob制限解除後のrate見直し(ネイティブ/バナーはAdMob優先に戻す選択肢、レクタングルはZucksのまま)
2026-06-10
iOS: YouTube埋め込みのアプリ内インライン再生化(外部アプリ遷移を解消)
動画詳細のYouTube埋め込み枠をタップすると YouTube アプリに飛ばされる/フルスクリーン化する問題を修正。アプリ内の埋め込み枠でそのまま再生できるようにした。
対象(iOSリポ 7channel-ios、別CodeCommit、master):
AdvancedWebView.swift:WKWebView()を設定つき生成に変更。allowsInlineMediaPlayback = true(iPhoneはデフォルトfalseでインライン再生不可)+mediaTypesRequiringUserActionForPlayback = [](autoplay有効化)。YoutubePlayerView.swift: iframe をplaysinline=0→playsinline=1に、allow="autoplay; encrypted-media; fullscreen"+allowfullscreen付与、自己閉じ/>を></iframe>に修正。
ハマりどころ(エラーが段階的に変化):
- まず
playsinline=1+ inline許可だけでは エラー153(動画プレーヤーの設定エラー)。原因はloadHTMLString(..., baseURL: nil)で origin がnull扱いになり埋め込みが拒否されること。 baseURLをhttps://www.youtube.comにすると今度は エラー152-4(この動画は再生できません)。youtube.com 自身を origin にすると同一オリジン扱いで弾かれる。- 解決:
baseURLと iframe のorigin=を自前ドメインhttps://7channel.ikapps.comで一致させたら再生成功。&enablejsapi=1&origin=...も付与。 - 要点: WKWebView の
loadHTMLString埋め込みは Referer/origin を実在の自前ドメインで揃えるのが肝。
管理画面URLの実態確認 + 旧v2インスタンス放置の発見
YouTube修正の流れで「管理画面URLは?」を調査したところ、ドキュメントと実態のズレが判明。
- 現行の管理画面URL =
https://7channel.ikapps.com/admin(HTTP 200・HTTPS)。<title>7channel 管理画面</title>確認済み。 - ドキュメント(
multi-purpose-lightsail-server1の README/.devnotes、v3deploy.sh)に書かれたhttp://7ch.ikapps.com/adminは DNS消滅(NXDOMAIN)で到達不能。2026-05-04 の集約時に更新漏れ。 - 旧v2 Lightsailインスタンス
13.230.63.19(静的IP7channel-v2-ip)が削除されず起動しっぱなしで発見。Welcome to nginx!のデフォルトページのみ返す素の箱(/adminは Host付きでも404=アプリ未搭載)。集約ゴール「Lightsail 1台」に反する残骸でコスト無駄の可能性 → 棚卸し対象(ROADMAP追記)。 - deploy.sh の
SERVER=13.192.166.254も応答なしで古い。
2026-06-07
App Store 審査提出を完了(年齢制限・輸出コンプライアンス・リリースノート)
近代化ビルド(8.3.0)のアップロード後、提出フローの残作業を片付けて審査提出まで完了。
- 年齢制限(Apple新方式の7ステップ): 実態どおり申告 → 18+ に確定。
- 機能: ユーザ生成コンテンツ=はい(タグ/コメント)、広告=はい、無制限Webアクセス=いいえ、ペアレンタル/年齢確認=いいえ。
- 成人テーマ: 汚い言葉=まれ / アルコール等=まれ / ホラー=なし。暴力=全てなし。
- 運/偶然: 疑似ギャンブル=頻繁(→18+の決定打)、実ギャンブル=いいえ(重要:はいにすると実賭博アプリ扱いでリジェクト)、コンテスト/ルートボックス=なし/いいえ。
- 韓国(GRAC番号)・アフガニスタンの販売制限警告は対象外市場ゆえ無視。
- 輸出コンプライアンス(暗号化書類): Info.plist の
ITSAppUsesNonExemptEncryption=falseで対応済み → 書類提出不要。一覧の2015年の失敗記録は無関係な残骸。 - リリースノート(日本語・必須): 「最新iOS対応・動作基盤刷新で安定性向上・広告の仕組み更新・軽微な不具合修正」のユーザー向け文面を記入。
- → 審査提出完了。あとは Apple の審査結果待ち。
iOSアプリを Xcode 26 向けに全面近代化(塩漬け→実機起動→App Store提出)
※ 6/6 の多角サービスレビューを起点に、その改善案①②に着手した流れ。
経緯(レビュー → 実装):
- まず large-scale-service-review を実施(GA4 + App Store Connect の一次データ)。所見を
review-20260606.md/.htmlに保存(operationリポにコミット済み 269c0df)。- 核心: DAU 7,000〜9,000・平均セッション7分・継続:新規=20:1 と粘着は一級品なのに収益ほぼゼロ。原因は広告メディエーションが死んでいること(nendは2023終了、AdMob SDKが2020年版で凍結)。
- 99%が日本のパチスロ動画ファン。新規流入は枯渇(レビューも2020年で停止)。
- レビューの改善案①(広告のAdMob集約)②(画面計測の復活)に着手 → そもそも塩漬けで最新Xcodeでビルド不能と判明し、近代化リビルドへ発展。
やったこと(iOSリポ 7channel-ios、別CodeCommit):
- 地取り: コミット済み現状を Xcode 26 でビルド → RealmSwift等が deployment target 8/9 で全滅と確認。
- 依存全面更新(Podfile刷新 + pod install): AdMob 7.62→13.4 / Firebase 6→12.14(+Crashlytics) / Alamofire 4.9→5.11 / Realm 5.2→20 / OneSignal 2→5(XCFramework) / SDWebImage・SwiftyJSON・Zucks・AdGeneration 現行化。Fabric/Crashlytics(旧)/GoogleIDFASupport/nend 撤去。deployment target を iOS 15 に統一。
- ビルドシステム対処: OneSignal拡張の二重コピー競合(拡張を
inherit! :search_paths)、macOS 26 の openrsync が dual-arch コピーで失敗する件(テストは arm64 単一指定で回避)。 - 広告をAdMobに集約(改善案①): 終了済みの nend / i-mobile を撤去し AdMob にフォールバック。埋め込み旧framework 4種(FB/Five/iMobile/Nend)と非互換アダプタ
libAdapterIMobile.aを pbxproj から除去。AdMob v7→v13 API移行(GAD接頭辞廃止・Unified→Native・Interstitial新API・DFP→GAM)。 - 画面計測を復活(改善案②): コメントアウトされ空だった
sendScreenView()を Firebasescreen_viewで実装 → GA4の画面名(not set)99%問題の根治。 - API破壊修正: OneSignal v5 / Alamofire 5(
SessionManager→Session等13ファイル)/ Realm 20(KeypathSortable制約・init override)。AppDelegate初期化刷新。通知拡張は v5移行で一旦パススルー化。 - シミュレータ+実機(Release署名なし) 両ビルド green を確認。
- 実機実行: 署名は Manual+無いプロファイル指定で Profile エラー → 全構成 Automatic署名に統一。Apple ID(tec4@thomsons.jp) ログイン後に自動署名ビルド成功、iPhone 14 へインストール&起動確認。
- App Store提出: Info.plist に
NSUserTrackingUsageDescription・ITSAppUsesNonExemptEncryption追加。通知拡張bundle IDが旧Teamに押さえられ配信登録失敗 →...NotificationServiceExtension3に変更。You側でアーカイブ→アップロード完了。
気づき・ハマり:
- 端末に入っていた現行版は prefix
HV2598SQ7Y(TOMSON, K.K.)だったが、現在の本番はJFBRCYHN24(Thomson Inc.)。アプリはアカウント移管済みで、旧版が端末に残っていたため混乱した。 - 移管の副作用でキーチェーン(
HV2598SQ7Y→JFBRCYHN24)が引き継げない。保存物は匿名の投稿者トークン1つ(タグ/コメントの本人判定用)だけなので実害小(既存ユーザーは過去の自分のタグ/コメントを編集/削除できなくなる程度)。 - CocoaPods の CDN が HTTP2 framing error を頻発 → lock削除して
pod installで回避。最新化は lock のピンに阻まれるので注意。
コミット: iOSリポ master に 4407ff9(近代化)/ 09a98bf(bundle ID修正)を push 済み。
2026-05-04
セッション - v2 (7channel-server-v2) を v3 に完全吸収
やったこと:
① SNS publish 権限の事前確認
- v2 の
notify.pyがSMS_Alertトピックに publish していることを確認 programs.topic_arn80 件は実体不在(過去削除されたまま放置)でハマったが、本命は.envのSNS_TOPIC_ARN(共通のSMS_Alert)だと判明- 過去 cron.log に 4/25 時点で
AuthorizationError(多目的Lightsail のAmazonLightsailInstanceRoleにはsns:Publish権限なし)→ 4/26 以降は~/.aws/credentialsを置いて IAM UserIchiro認証に切替で publish 成功していた経緯を確認 - Lightsail Micro 側も同じ
IchiroIAM User → publish 確実に通る(テスト publish で[TEST] 7channel SNS 移行確認メールが届くこと確認) SMS_Alertの subscribers:goda.takeshichan2@gmail.comとichirokisanuki.apple@gmail.com(メインichirokisanuki@gmail.comは subscribe されていない、これまでの通知は.apple@gmail.comに届いていた)
② v3 に curate を FastAPI ルーターとして移植
- v3 には既に
notify.py/database.pyの関連ヘルパ /youtube.py/ admin 画面(programs/casts/movies CRUD)が揃っており、残作業はcron/curator.py新規追加のみ - 独立スクリプトではなく FastAPI ルーター
/cron/curateに統一(他の cron と同じ呼び出し方) - SNS publish は try/except で握り潰し、失敗しても curate 全体が失敗しないよう防御
- Lightsail Micro へデプロイ → 1回手動実行で 0.28 秒で「処理対象1件 → 採用1件」の動作確認
- 通知メール本文の管理画面リンクを
http://13.230.63.19:8000/admin→https://7channel.ikapps.com/adminに更新
③ Lightsail Micro の crontab に追加
*/10 * * * * curl -s http://127.0.0.1:8000/cron/curate >/dev/null 2>&1- 自分自身 loopback なので Nginx/SSL 不要、軽量
④ 多目的Lightsail の v2 痕跡を完全撤去
- ubuntu crontab から
curate.py行を削除(残ったx-account-manager-aicron は別プロジェクトなので維持) 7channel-web.service(v2 uvicorn を port 8001 で動かすやつ)を stop & disable & unit file 削除 & daemon-reload/home/ubuntu/7channel-server-v2/ディレクトリ削除- Nginx
sites-available/7ch.ikapps.comと sites-enabled シンボリックリンク削除(proxy_pass http://127.0.0.1:8001で死にプロキシ状態だった)→nginx -t通過 → reload - Route53
7ch.ikapps.com A 13.230.63.19レコード削除(boto3 経由)
⑤ ローカル整理
git rm -r 7channel-server-v2/で 16 ファイル削除- 残った untracked
venv/をrm -rfで削除 - コミット 097b336 / 03f379c
気づき:
- v3 が想像以上に v2 と同じヘルパ・admin を既に持っていたので、curate.py 移植は実質1ファイル追加で済んだ
- SNS の subscriber リストを把握しないままテスト実行すると「届いてない」と誤解する。事前に list_subscriptions_by_topic で subscriber 一覧を確認しておくべき
programs.topic_arnのような未使用カラムが DB に残ってるとハマりの元。ただし今回は片付け対象外(過去 80 番組分のメタデータ、いつか再開する可能性もある)
コスト・安定性影響:
- コスト変化なし(v2 の停止は CPU/メモリの解放のみ)
- 多目的Lightsail から 7channel 関連の依存はゼロ。7channel = Lightsail Micro 1台に完全集約
次やること:
- 数日観察(v3 cron / curate の継続動作、SNS 通知が10分毎に届くか)
セッション - 完全別鯖の cron を Lightsail Micro に統合
やったこと:
- 完全別鯖(
52.194.87.168、別用途で継続稼働、停止はしない)から毎分叩いていたcron.txt16行を7channel-server-micro(35.73.116.227) の ubuntu crontab に移植 - 各行に
>/dev/null 2>&1とMAILTO=""を付与してメール抑止 - URL は
https://7channel.ikapps.com/...のまま(外形監視も兼ねる) - 移植後 1 分待って syslog で CRON 発火確認、access.log で self-curl が NAT 経由で
35.73.116.227/3.172.52.231から記録されるのを確認 - ユーザー側で旧別鯖の crontab から 7channel 行を削除 → access.log で
52.194.87.168の最終ヒットが09:42:06 UTCで停止したのを確認
気づき:
- Lightsail Micro の負荷は load avg 0.00 で余裕あり、cron 16行追加しても影響なし
- self-curl は loopback ではなく NAT 経由で外を回って戻ってくる → 外形監視を自然に兼ねる構成になった
- macOS の
date -d '5 minutes ago'は GNU date 拡張で BSD date では動かない(過去ログ抽出スクリプト書く時のハマりどころ)
次やること:
- 数日観察(cron が継続稼働しているか、load 増加傾向がないか)
セッション - cutover後の隠れバグ総ざらい(cron復旧/quota防御/JST化/utf8mb4化/v2 DB_HOST修正)
やったこと:
① v3 cron が strict mode で500エラー連発 → 旧コードを修正
- 「動画が追加されない」と気づいて調査開始
- 別鯖(
52.194.87.168)から毎分 cron が叩かれていることはアクセスログで確認できたが、/cron/movieUploadsClassic/1/api3/updateHotMovies/1等が 500 エラー - journalctl で
Field 'created' doesn't have a default value(1364) とIncorrect datetime value: '0000-00-00'(1292) - 旧 kizuna RDS は緩い sql_mode で運用されていたが、Lightsail の MySQL 8.0.45 はデフォルトで
STRICT_TRANS_TABLES,NO_ZERO_DATE有効 - 4ファイル修正してデプロイ:
cron/youtube_uploads.py:79movie_uploads.upload_dateの初期値'0000-00-00'→'1970-01-01 00:00:00'cron/youtube_uploads.py:223moviesINSERT にcreated/updated/poplar_degree_updatedを追加cron/hot_movies.py:59hot_moviesINSERT にcreated/updatedを追加cron/article_crawl.py:69articles.dateNULL フォールバックを'1970-01-01 00:00:00'にdatabase.py:44insert_movieヘルパも同様に補強
- コミット 842b366
② YouTube Data API quota 超過と「全動画削除済み」誤判定の連鎖障害
/cron/movieUploadsDetailで API が 403quotaExceededを返した瞬間から、コードがpageInfo.totalResults=0フォールバックで「YouTube側で削除済み」と誤判定してdel=1を押し続けていた- cutover 後の新着が次々と「削除済み」扱いに(07:00 UTC 以降に8件)
- コード防御を追加:
if r.status_code != 200 or "error" in data: continue - 新規 Google Cloud プロジェクトで取得したキーを
YOUTUBE_API_KEY_3として.envに追加(同一プロジェクト内のキー追加では quota 共有で意味なし、新プロジェクトを作ってキー発行する必要があると判明) movieUploadsDetailをYOUTUBE_API_KEY_3参照に切替- 誤って del=1 にされた8件を
del=0に戻す救済 SQL 実行 → 6件は次の周期で正しく title/upload_date 取得済み - コミット a3c40e4
③ ローカルで 7channel-server(PHP)を動かす環境整備
app/Config/database.phpの host が削除済み kizuna エンドポイントのまま → 「DB なしで動かしたい」要望だが CakePHP は Model 前提なので不可能- SSH トンネル経由 Lightsail MySQL 接続を試したが、PHP コンテナ環境(Docker)から
127.0.0.1だと届かず →host.docker.internalに - 接続は通ったが MySQL 8 デフォルト collation
utf8mb4_0900_ai_ci(ID 255) を古いmysqlndが認識できず[2054] Server sent charset unknown to the clientで詰まる - 方針変更: ローカルに MariaDB を入れる(charset 問題に当たらないレガシー PHP と相性良)
brew install mariadb→7channelDB と user 作成 → Lightsail からスキーマのみ (mysqldump --no-data) を取得・流し込み(68テーブル、データは空)ngramフルテキストパーサーは MariaDB に無いので sed で除去7channel@%だけでは不十分で7channel@localhost7channel@127.0.0.1も追加(MariaDB はlocalhost接続を別扱い)
④ utf8mb3 → utf8mb4 文字化け対応
descriptionカラムに絵文字(🔻 = 4バイト UTF-8)が来て[1366] Incorrect string value発生movie_uploadsmoviesをCONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci実行- ALTER 中に旧 PHP 時代の
'0000-00-00 00:00:00'データを strict mode が弾いたのでSET SESSION sql_mode='NO_ENGINE_SUBSTITUTION'でリトライ → 成功
⑤ OS と MySQL を JST 化
timedatectlで UTC のまま、MySQL のsystem_time_zoneも UTC でNOW()が UTC を返してたsudo timedatectl set-timezone Asia/Tokyo→ MySQL 再起動 →7channel-web再起動- v3 コードで YouTube
publishedAt(常に UTC で来る)をastimezone(ZoneInfo("Asia/Tokyo"))で JST 変換してからupload_dateに格納するよう修正 - 過去データ(UTC で入った行)には触れず、今後の INSERT/UPDATE のみ JST に
⑥ v2 curate.py の DB_HOST 取り残し
- 「69844 がなぜ movies に追加されない?」という問いから判明
- 多目的Lightsail で 10分毎に動いている
7channel-server-v2/curate.py(programs.matching_wordベースで自動振り分け)の.envのDB_HOSTが削除済み kizuna エンドポイントのまま cron.logを見たらUnknown MySQL server host 'kizuna...'で 10分毎にエラー終了していた.envをDB_HOST=172.26.8.107(Lightsail Micro 内部IP)に変更 → 手動実行で「採用 7 件 / 却下 3 件」復活- 69844 も
program_id=522(俺たちタッグだろ) で正しくマッチして movies 入り
気づき・教訓:
- 「サーバー移管時の周辺要素チェック」memory に sql_mode / DB_HOST 取り残し / OS+MySQL TZ の3パターンを追記
- DB引っ越し時は旧エンドポイント文字列で
~/cdev/と各サーバーの.env全部を grep してから旧 DB を削除する手順が必要 - YouTube Data API の quota はプロジェクト単位(10,000/日)。キーを複数発行しても同一プロジェクトなら共有
- ローカルレガシー PHP テストには MariaDB が無難(MySQL 8 collation 問題回避)
host.docker.internalで Docker コンテナからホスト Mac の MySQL に到達可能
コスト・安定性影響: ゼロ(コード修正+設定変更のみ)
次やること:
- 数日観察(v3 cron / curate.py / 新着が movies に流れているか)
- dev-timer #13 (5/11 11:00) で RDS 最終スナップ削除
- dev-timer #12 (6/1 11:00) で CloudFront 削除再挑戦
セッション - 古いEBSスナップショット全削除+RDSスナップ削除リマインド
やったこと:
- AWS アカウントに残っていた古いEBSスナップショットを総点検
- 11個の AMI(2013年〜2019年の旧サービス: 7channel初代 / 7ch-old-final / FootballNEXT / bizpice / news / tokyosoftware.info / haiena / current / current3 / laravel-tochu / COMMON_IMAGE_20170110)と紐付くもの
- AMI 未参照の単独スナップショット 1個(
before delete、2026-04-04 作成)
- AMI 未参照の単独スナップショット 1個(
- 合計 12個 / 96GB → 月約 $4.8 課金されていた
- AMI を全11個 deregister → スナップショット 12個全削除
- RDS最終スナップショット
kizuna-final-before-delete-20260504(9GB / 月$0.86)は1週間後に削除予定- dev-timer #13 を 2026-05-11 11:00 にセット
気づき:
- AWS で過去の AMI/EBSスナップショットが13年前から残っていた。AMI 自体は無料だが、紐付くEBSスナップショットは月課金され続ける($0.05/GB/月)
- 古いプロジェクト(FootballNEXT、bizpice 等)のサービス終了時にスナップショットを消し忘れる事故が多発。今回まとめて削除
コスト効果:
- EBSスナップショット削除で 月 -$4.8(年 -$58)
- RDSスナップ削除予定(5/11)で更に 月 -$0.86(年 -$10)
残TODO:
- dev-timer #12: 2026-06-01 11:00 — CloudFront ECQ9QLPK0563T 削除再挑戦
- dev-timer #13: 2026-05-11 11:00 — RDS最終スナップショット削除
セッション - 最終ゴール達成(RDS Lightsail移行+CloudFront廃止+Let's Encrypt直接HTTPS)
やったこと:
RDS テーブル整理(Lightsail移行前のスリム化)
cast_imps(5,372,939行 / 250MB): SELECTゼロの死テーブル、INSERTも停止済み → DROParticle_imps(1,048,833行 / 50MB): 同じパターン、v3 のimpressionArticleを no-op化してINSERT停止 → DROPkabutan_*系3テーブル: 株系の機能、未使用 → DROPtwitter_follow_candidates / twitter_followers_charts / twitter_follows: Twitter自動フォロー機能、停止中 → DROP- 計 約 313MB 削減、69テーブル / 500MB に
Lightsail Nano → Micro アップサイズ
- MySQL 同居で 512MB は OOM リスク → Micro (1GB RAM、$7/月) にアップサイズ
- スナップショット → 新Micro 起動 → 静的IP切替 → 旧Nano削除 の流れ
- Lightsail はインプレース変更不可だが、static IP 引き継ぎ + 同じ blueprint で実質ダウンタイムなし
MySQL 8 セットアップ + RDS データ移行
- Lightsail Micro に MySQL 8.0.45 インストール、
innodb_buffer_pool_size = 128M等チューニング - kizuna RDS から
mysqldumpで 362MB ダンプ - 1回目 import 中にOOM → swap 1GB 追加して再試行
- 2回目(高速モード
SET autocommit=0; SET unique_checks=0;)で成功(7分) - 68 テーブル / 550MB / movies 117,799行 / articles 313,650行 移行成功
- v3 の
.envでDB_HOST=127.0.0.1に切替、systemd restart で本番反映
phpmyadmin の接続も Lightsail MySQL へ
- Lightsail 同士は内部VPC (
172.26.0.0/16) で接続可能 → MySQL をbind-address = 0.0.0.0、7channel@172.26.%ユーザー追加、Lightsail FW で 3306 を172.26.0.0/16のみ許可 - phpmyadmin config の host を
172.26.8.107(Micro Private IP)に ichirokisanuki管理ユーザーが import に含まれてなかったので、Lightsail MySQL に同パスワードで作成(GRANT ALL ON *.* WITH GRANT OPTION)
kizuna RDS 削除
- 最終スナップショット
kizuna-final-before-delete-20260504作成(保険) - Deletion Protection が ON → modify で OFF にしてから delete
- delete-db-instance 完了確認、AWSアカウントから kizuna が完全消滅
- 月 -$20 確定(RDS本体)+ Public IPv4課金も停止
CloudFront 廃止 → Let's Encrypt 直接HTTPS
- Lightsail に
python3-certbot-dns-route53インストール - DNS-01 challenge で
7channel.ikapps.comの Let's Encrypt 証明書取得(自動更新設定済み) - Nginx を 443 listen + SSL 設定、80→443 リダイレクト
- Lightsail FW で 443 を開放
- Route53
7channel.ikapps.comの A レコードを CloudFront ALIAS → Lightsail static IP35.73.116.227に変更 - 本番動作確認:
https://7channel.ikapps.comが 200 OK / 60ms(CloudFront経由より速い) - Route53
lightsail.7channel.ikapps.com(CloudFront orig 用)も削除
CloudFront 削除トラブル
- ECQ9QLPK0563T を Disable した後 Delete を試みたら "subscribed to a pricing plan" エラー
- Security Bundle (WAF) 課金が残ってて、解除しても月末まで Delete 不可と判明
- Disabled のまま放置(実害ゼロ・トラフィックゼロ)
- dev-timer #12 で 2026-06-01 11:00 にリマインド設定
気づき:
- Lightsail Nano (512MB) で MySQL同居は OOM 確実 → Micro (1GB) でも swap 必須
- Lightsail インスタンスのスナップショット経由アップサイズは数分でできる、静的IP切替で DNS変更不要
- Lightsail 同士は内部VPC (
172.26.0.0/16) で繋がる、外部公開せずに DBアクセス可能 - CloudFront Security Bundle は 1ヶ月課金固定、解除→月末待ちが必要
- DNS-01 challenge は Route53 plugin で完全自動化(IAMユーザーのアクセスキーで認証)
コスト削減(最終):
最終月コスト: 約 $7(Lightsail Micro のみ)+ Route53/snapshot 微小
削減: $106 → $7 → 月-$99(年-$1,188)
CloudFront を月末に削除すれば更にゼロに
最終構成:
iPhone → 7channel.ikapps.com (Route53 A → Lightsail static IP)
↓
Lightsail Micro $7 (Nginx 443 SSL/Let's Encrypt → uvicorn 8000)
↓ 同居
FastAPI v3 + MySQL 8.0
↓
S3 channel7/current/201609 (iOSアプリ用基本データ)
セッション - Phase D 完遂(CloudFront切替・B-6緊急実装・クリーンアップ)
やったこと:
CloudFront origin 切替(cutover完了)
- ユーザーがCloudFront コンソールで origin を
lightsail.7channel.ikapps.comに切替・Deployed確認 https://7channel.ikapps.com経由で v3 (Lightsail) が応答することを確認- iOSアプリ「7ちゃんねる/8.2.0.3」(CFNetwork UA) の本物リクエストが Nginx ログに着弾、200 OK
- CloudFront経由で
Miss from cloudfront(CachingDisabled が正常動作)
B-6 緊急実装(保留してたが実は必須だった)
- iOSアプリ "Avatar3"(jp.thomsons.aaa.aaa)と cron (curl/7.53.1) の両方が
/api2/beginBroadcastを叩いていて 404 連発 - 元 PHP は
/S3Uploader/current201609を呼んで全テーブルから集計したJSONを S3 (channel7/current/201609) に public-read で put する処理 - iOSアプリは S3 から直接基本データ(top_movie / hot_movie / cast / channel / device 等)を取得する設計と判明 → 保留できる代物ではなかった
cron/s3_uploader.pyを新規実装(22テーブル分の集計、boto3 で S3 put、355KB JSON)- Lightsail に
~/.aws/credentialsで IAM ユーザー認証情報配置 /api2/beginBroadcastと/S3Uploader/current201609を v3 に追加・デプロイ- 実際に S3 アップロード成功(
s3://channel7/current/201609、Content-Length 355,814、public-read で iOSアプリから取得可能)
castsフィールド の調査と修正
- Cutover直後 castsが全部空だった件を調査 → 原因2つ:
- v3 の
attach_castsが PHP版にないcast_movie_relations.del = 0フィルタを入れていた → 削除して PHP互換化 - 新着動画(id ≥ 125384)はそもそも
cast_movie_relationsに未登録のため空が正解
- v3 の
- id=125383 で
casts="20,21,140"の取得を確認 → v3 動作正常
iOSアプリの古いデータ表示問題
- アプリで先頭が id=125382 表示 → 12:00〜20:22 の cron 404期間中に S3 が古いまま、アプリのキャッシュも古い
- アプリ完全終了→再起動で 125586(最新)が先頭表示に → 解決
軽微な改善(B案)
/v1/healthcheck200 OK 追加(旧 V1Controller 互換、senrigan-observer 用)- HEAD method を GET にマッピングするミドルウェアを追加 → CloudFront origin healthcheck の
Error from cloudfront解消、HEAD で 200 OK 返却
クリーンアップ(A案)
- EC2-2 (
i-01b0e45af32e40061) の Elastic IP43.206.39.55を disassociate + release - EC2-2 terminate 完了
- RDS
old_movie_imps(10,424,551 行 / 967.61 MB)を DROP - 本番疎通すべて 200 OK 確認(/health / /api3/* / /v1/healthcheck / /api2/beginBroadcast)
phpmyadmin(mydb.ikapps.com)副次対応
- EC2-2 を terminate したことで kizuna RDS への SSH トンネル踏み台が消失 → phpmyadmin から kizuna 接続不可
- kizuna は今 PubliclyAccessible=true なので、SSH トンネル不要で直接接続できることを利用
- 多目的サーバー static IP
13.230.63.19/32を kizuna RDS SG に許可 mydb.ikapps.comの/var/www/phpmyadmin/config.inc.phpを127.0.0.1:13308→kizuna.cxboespy3b8c.ap-northeast-1.rds.amazonaws.com:3306に変更phpmyadmin-tunnel-ch7.serviceをsystemctl disable --nowで停止・自動起動解除nc -vzで TCP 疎通確認 OK- 詳細は
~/cdev/common-php-my-admin/.devnotes/側で別途記録
気づき:
- B-6 (beginBroadcast) は「cron頻度低いし保留可」と判断してたが、iOSアプリが S3 経由で基本データを取得する設計なので保留不可だった。事前に「このエンドポイントは何のために存在するか」をもっと深掘りすべきだった
EC2-2 terminateの前に「このサーバーは他の用途で使われていないか」を確認するチェックリストが必要。今回 phpmyadmin の SSH トンネル踏み台になっていた事実を踏まえ漏らした- CloudFront キャッシュ動作と CachingDisabled の関係: GETリクエストは
Miss from cloudfrontと表示され、毎回 origin に到達。HEAD は別の挙動だが middleware で対応。
コスト削減効果(累計)
| Before | 段階1+2後 | 段階3後(今) | |
|---|---|---|---|
| 月コスト | $106 | $69 | 約$45 |
| 削減累計 | - | -$37 | -$61/月(年-$732) |
最終構成:
iPhone → CloudFront → Lightsail Nano (Python/v3) $5
↓
kizuna RDS $20
次回やること:
- 数日観察(5xx・cron・iOSアプリ)
- 将来: kizuna RDS を Lightsail MySQL に移行(更に -$20、最終形に到達)
2026-05-03
セッション - Phase D 着手(Lightsail立ち上げ・v3デプロイ・cutover直前で revert)
やったこと:
- Phase D 開始。最終ゴール(Lightsail 1台集約)に向けて新インスタンス立ち上げ
~/cdev/schulte-operationの前例を検証 → schulte の RDS(footballnext)は default VPC にいたから VPC peering で接続できたが、kizuna は カスタムVPCvpc-4b77802eにいるため Lightsail VPC peering 不可と再確認- ユーザー判断で「RDS Public化 + Lightsail static IP を SG許可」案(B案)を採用
- Lightsail Nano
7channel-server($5/月、Ubuntu 22.04、ap-northeast-1a)作成- 静的IP
7channel-server-ip(35.73.116.227) を allocate + attach
- 静的IP
- RDS
kizunaの PubliclyAccessible を false → true に戻し、SGsg-a35895c6に35.73.116.227/32で 3306 を許可 - Lightsail に v3 をデプロイ:
- rsync で
7channel-server-v3/を~/7channel-server-v3/に転送 - venv 作成、
pip install -r requirements.txt - Nginx インストール、
80 → 127.0.0.1:8000リバプロ設定 - systemd unit
7channel-web.serviceを作成・有効化
- rsync で
- 外部疎通確認OK(http://35.73.116.227/health → 200、
/api3/nextMovieData?limit=2の id=125383 で PHP本番と一致) - Route53 Aレコード作成:
lightsail.7channel.ikapps.com→35.73.116.227(CloudFront のオリジンに IP は使えないため) - CloudFront origin 切替: ユーザーがコンソールで CloudFront ECQ9QLPK0563T のオリジンを
lightsail.7channel.ikapps.comに変更 → 即時、「明日に進めたい」とユーザー判断で原状に revert- 再度オリジンを
ec2-43-206-39-55.ap-northeast-1.compute.amazonaws.comに戻す手順を案内
- 再度オリジンを
気づき:
- Lightsail VPC peering は default VPC とのみしかできない仕様(公式制約)。schulte の RDS は default VPC、7channel の kizuna はカスタムVPC、という違いで前例が直接使えなかった
- CloudFront のオリジンに IPアドレスは指定不可(DNSドメイン名のみ)。Lightsail の static IP を origin にするには Route53 の A レコード経由が必要
- Lightsail インスタンスは EC2 のような publicly resolvable hostname (
ec2-X-X-X-X.compute.amazonaws.com) を持たない。internal hostname(ip-172-26-4-27.ap-northeast-1.compute.internal)のみ - IAMユーザー
Ichiroには CloudFrontGetDistributionConfig権限すらないため、CLI からの update不可。コンソール操作必須
Phase D 残作業(明日):
- 朝にLightsail/v3 が安定稼働してることを確認
- CloudFront origin を
lightsail.7channel.ikapps.comに再切替(ユーザーがコンソール操作) https://7channel.ikapps.com経由で動作確認、iOSアプリ実機テストも- 数時間〜1日観察(5xx・レイテンシ・cron動作)
- 問題なければ:
- cron.txt は URL同じなので変更不要(CloudFront経由でLightsailに自動的に流れる)
- EC2-2 terminate
- RDS の不要IPv4解放
old_movie_impsDROP
セッション - 段階3着手(v3 新規実装でPHP→Python移植、Phase A〜C 完了)
やったこと:
方針確認
- 「最終的にLightsail 1台でプログラム+DB集約」が最終ゴール
- まず PHP→Python 移植してからデータ移行する方針に決定
- PHP互換性問題を完全回避(PHP 5.3 + 旧CakePHP は実質移行不可能)
~/cdev/各プロジェクトの DEVLOG / memory を grep しschulte-operationの前例(PHP→FastAPI でLightsail移行、Route53 Weighted Routing で 10%→50%→100% カットオーバー)を発見・参考に
利用エンドポイント特定(access log 分析)
EC2-2 のアクセスログから iOSアプリが実際に使っているエンドポイントを抽出 → /api3/* の9個だけと判明:
| エンドポイント | 週次回数 |
|---|---|
/api3/nextMovieData |
2,026 |
/api3/impressionMovie |
793 |
/api3/nextMovieDataForCast |
361 |
/api3/impressionCast |
323 |
/api3/nextArticleData |
253 |
/api3/nextMovieDataForProgram |
184 |
/api3/nextMovieDataForChannel |
95 |
/api3/nextMovieDataForDevice |
51 |
/api3/nextMovieDataForHot |
38 |
Web/Portal/管理画面系 は iOSアプリ無関係と判明 → 移植不要
v3 ディレクトリを新規作成(v2 を吸収)
7channel-server-v2を7channel-server-v3にコピーして拡張- DBスキーマ抽出(19テーブル / 14KB)→
v3/schema.sqlに保存
Phase A: iOS API 9メソッド の FastAPI 実装
api/movies.py:/api3/nextMovieData系 6エンドポイントapi/articles.py:/api3/nextArticleDataapi/impressions.py:/api3/impression{Movie,Article,Cast}api/common.py: cast 集約(GROUP_CONCAT で N+1問題解消、PHP版より高速)- ローカル uvicorn 起動 + SSH トンネル経由でkizunaRDS接続して動作確認 → PHP本番と全エンドポイントの結果完全一致(id・件数・casts文字列まで)
Phase B-1〜B-5: cron 系 12メソッド実装
| サブPhase | ファイル | 内容 |
|---|---|---|
| B-1 | cron/maintenance.py + cron/rotate_offset.py |
cleanMovieImps / updateProgramMovieCount / updateCastMovieCount / updateCastLastMovieUploadDate |
| B-2 | cron/hot_movies.py |
updateHotMovies (4 type: 1日/1週/1月/全期間) |
| B-3 | cron/cleanup.py |
removeNotNeedMovieUploads |
| B-4 | cron/article_crawl.py + cron/relation_sync.py |
crawl/article + cron/articleImageScraping + crawl/movieCastRelationSync(feedparser + BeautifulSoup) |
| B-5 | cron/youtube_uploads.py |
movieUploadsClassic / movieUploadsDetail / autoEntryMovie(YouTube Data API v3 + RSS) |
全部ローカル動作確認済み。/cron/movieUploadsClassic/1 で実際に新規動画1件が DB に追加されることまで確認。
Phase C: v2 admin 機能の動作確認
/admin/admin/programs/admin/casts系 21エンドポイントすべて 200 OK- HTML レンダリング正常、341KBのページが生成される
- v3 で v2 の admin が完全動作
B-6 保留
/api2/beginBroadcastは中身ほぼコメントアウトで、唯一活きてるのは/S3Uploader/current201609(S3アップロード処理、複雑)- cron頻度が10分毎なので影響なく後回し可
クリーンアップ
- uvicorn・SSHトンネル停止
- 一時SSH SG エントリ削除
気づき:
- access log 分析でiOSアプリ用エンドポイントが9個しかないと特定できたのが大きい。60+ Controller 数百メソッドのPHPコードのうち、本当に必要なのはごく一部
- N+1 問題(PHP版で各動画ごとに cast を SELECT)を Python 移植時に GROUP_CONCAT で解消、自然にパフォーマンス向上
- PHP は数値カラムを文字列で返す傾向(
"125383")、FastAPI は数値(125383)。iOSアプリ側の JSON デコード型次第で互換性問題の可能性 → カットオーバー前に要確認
次回やること:
- B-6 (beginBroadcast の S3アップロード処理) は別途
- Phase D: Lightsail Small インスタンス作成 → MySQL/PHP/Python セットアップ
- データ移行(mysqldump → Lightsail MySQL)
- v3 デプロイ
- CloudFront のオリジンを Lightsail に切替(最終のカットオーバー)
- 動作観察後 EC2-2 + RDS を完全削除
2026-04-28
セッション - 段階2完了(CLB廃止 + CloudFront経由HTTPS + 関連リソース大掃除)
やったこと:
スロークエリ第3弾の検証とロールバック
- 第3弾デプロイ後のスロークエリログを確認 → STRAIGHT_JOIN化した
CastMovieRelation::getRowsが最大14秒・平均7.7秒という致命的問題を発見 - 原因推測: EXPLAINテストでは
cast_id=132(関連数多)で250msだったが、本番では関連数の少ないcast_idで叩かれる際に movies 駆動でLIMIT 30を満たそうとして大量スキャン発生 - ローカルで元の自然JOIN形式にロールバック修正→デプロイ済み
- 残スローパターン3つは平均1.3〜1.7秒で許容範囲と判断(コード書き換えはしない)
段階2: CLB廃止 + CloudFront経由(最終ゴールに向けた大整理)
- 多目的サーバーには 7channel を置きたくない(既に v2 + AI スクリプトが居て分離したい)方針に決定 → 改訂案: EC2-2はそのまま専用機として残し、CLBだけ廃止
- HTTPS問題発覚: CLBが ACM 証明書(ap-northeast-1)でSSL終端していた → 直EC2-2では別途SSL必要
- 当初 Let's Encrypt(A案)の方針 → Amazon Linux AMI 2018.03 がEOLで certbot 導入が現実的でない → CloudFront経由(B案)に方針変更(既存のACM証明書を us-east-1 に新規発行して流用)
- 実行:
- EC2-2 にElastic IP
43.206.39.55を確保してアタッチ(IP固定化) - us-east-1 で ACM 証明書発行(DNS検証・手動・コンソール)
- CloudFront ディストリビューション
ECQ9QLPK0563T作成(手動・コンソール)- 代替ドメイン:
7channel.ikapps.com - キャッシュポリシー: CachingDisabled、オリジンリクエスト: AllViewer、HTTPS リダイレクト
- Origin Protocol を https-only → http-only に修正(EC2-2 が80番のみだったため504発生 → 修正で解決)
- 代替ドメイン:
- EC2-2 SG に CloudFront prefix list (
pl-58a04531) を追加して80番開放 - Route53
7channel.ikapps.comのALIASを CLB → CloudFront に切替(hosted zoneZAJWFBI6JY6TR) - 6時間観察期間 → CloudWatch メトリクスでクリーン確認
- EC2-2 にElastic IP
- クリーンアップ:
- CLB
Channel7削除 - Lambda 4個削除(CMN_detachEC2FromELB / CMN_attachToELB / CMN_startEC2 / CMN_totalHealthCheck)
- EventBridge 9ルール削除(per1min / Per5Minute / Per10Minute / CH7_cronSendPickupPush / SLN_cron / testcron / CMN_checkLoadBalancer / CMN_recoveryForEC2 / CMN_recoveryToLoadBalancer)
- EC2-1(停止中・i-0e9fe6c9d4bca23ae)terminate
- CLB
- SSH SGクリーンアップ(再追加IPと前回IPを両方削除)
気づき:
- STRAIGHT_JOIN は危険: EXPLAIN・1ケースのテストで速くても、データ分布の違いで本番が劇的に遅くなり得る。JOIN順序の固定は最終手段
- IAMユーザー
Ichiroには ACM/CloudFront/Route53 の 書き込み権限がない部分がある(CloudFront作成は手動コンソール、Route53 はzone単位で許可されてるzoneだけCLIで通る) - CloudFront作成時の罠:
- Origin に IPアドレス直接は入れられない → AWS DNS名(
ec2-XX-XX-XX-XX....compute.amazonaws.com)を使う - Origin Protocol Policy は デフォルトで https-only → EC2 が80番だけなら http-only に変更必須
- Origin SG が CLB の SG しか許可してない場合、
com.amazonaws.global.cloudfront.origin-facingprefix list を追加する
- Origin に IPアドレス直接は入れられない → AWS DNS名(
コスト削減効果:
- CLB $20 + Public IPv4 $11-15 + EC2-1 EBS $1 + Lambda $1 = 約 $33-37/月(年間 $400-450)
- 残: EC2-2 ($20) + IPv4 ($3.65) + RDS ($20) ≒ 月$45 → 段階3で更に削減予定
次回やること:
- 段階3: RDS
kizunaを多目的サーバーのMySQLに移行(2.3GB、月-$20見込み) - PHP 5.3 + 旧CakePHPアプリのリプレース方針決定
old_movie_imps(約1GB)のDROP(Lightsail移行と一緒に)
2026-04-27
セッション - スロークエリ第3弾(device_id等の絞り込みパターンに対処)
やったこと:
- 第2弾デプロイ後数時間のスロークエリログを集計 → 第2弾ワースト1〜3 すべて消滅確認
- 新たに浮上した3つのパターンを EXPLAIN で原因特定:
- A:
MovieCastRelation cast_id+delJOIN movies ORDER BY upload_unixtime(CakePHPgetAll、Api3Controller::nextMovieDataForCast)→ 950ms - B:
CastMovieRelation::getRows(cast_movie_relations 経由、ORDER BY upload_date DESC, id DESC LIMIT 30 OFFSET 0、Web側で使用)→ 1.7秒 - C:
movies WHERE device_id=? AND del=0 ORDER BY upload_date DESC LIMIT 30(CakePHP recursive=1 + 全カラム LEFT JOIN)→ 7.6秒
- A:
- インデックス追加で対処:
movies.idx_device_del_upload (device_id, del, upload_date)→ C: 7.3秒 → 62msmovies.idx_channel_del_upload (channel_id, del, upload_date)→ 同パターン予防movies.idx_del_upload_unix (del, upload_unixtime)→ A: 950ms → 270ms
- アプリ側書き換え:
- B:
Model/CastMovieRelation::getRowsを STRAIGHT_JOIN 形式に書き換え(movies 駆動 +idx_del_upload活用)→ 1.7秒 → 数百ms、デプロイ済み - A: コード書き換えは見送り(CakePHP
getAllのレスポンス形式変更リスク。270ms=閾値1秒未満で実用上OK)
- B:
- dev-timer 2件登録:
- 1時間後にテスト用通知
- 明日朝11:00 にスロークエリ第3弾の効果確認通知
気づき:
- 同じ「ID列での絞り込み + ORDER BY upload_date」パターンが program_id, device_id, channel_id とテーブル横断で出てくる
→
(<id_col>, del, upload_date)複合インデックスが対症療法として効く - アプリ書き換えはレスポンス形式が変わる可能性があり、API契約への影響リスクがある
→ CakePHP
find/getAll経由で結果オブジェクト形式に依存する箇所はインデックス対応のみが安全 - 直接 SQL を組み立ててる Model メソッド(
getRowsなど)は STRAIGHT_JOIN への書き換えが安全かつ効果的
次回やること:
- 明日朝11:00(dev-timer 通知時)にスロークエリログ再確認 → A/B/C パターン消えてるか
cleanMovieImpscron 初回実行確認- SSH SG クリーンアップ(自分のIP 2件)
セッション - スロークエリ第2弾(新ワースト1〜3 をインデックス追加で全解消)
やったこと:
- デプロイ後数時間経過のスロークエリログを集計確認 → ワースト1〜3(旧)全部消滅を確認
- 新たに浮上したワースト1〜3 を EXPLAIN で原因特定し、すべてインデックス追加だけで対処:
- 新ワースト1:
Movie.program_id = ? AND del = 0 ORDER BY upload_date DESC LIMIT 30の filesort →moviesにidx_program_del_upload (program_id, del, upload_date)追加 → 5.3秒 → 8ms - 新ワースト2:
cast_movie_relations経由の Movie 取得(前回ORDER BY upload_date DESCに書き換え後も filesort 残ってた) →moviesにidx_del_upload (del, upload_date)追加 → 1.7秒 → 21ms (EXISTS 書き換え・STRAIGHT_JOIN も試したが、元のクエリ + 新インデックスが最速だった) - 新ワースト3:
articles.url = ? LIMIT 1の URL 重複チェック(24万行フルスキャン) →articlesにidx_url (url)追加 → 約2秒 → 2.7ms
- 新ワースト1:
- SSHホワイトリストに今回のグローバルIP
220.215.166.104/32を追加(前回133.32.128.92/32のIPは既に変わっていた) - iOSアプリのアイコンファイル位置を確認:
7channel-ios/Channel7/Assets.xcassets/AppIcon.appiconset/(本命はicon-60@3x.png)
気づき:
- スロークエリ対策はアプリのコード書き換えより、適切な複合インデックス追加で済むケースが多かった
- EXPLAINで
Using filesort/Using temporaryが出ていたら、ORDER BY を含む複合インデックスで解消できることが多い - EXPLAIN の表示が同じでも、内部的な処理(インデックス活用)で実測時間が桁違いに変わることがある(EXISTS 書き換え不要だった件)
次回やること:
- SSH SGの仮追加IP(
133.32.128.92/32と220.215.166.104/32)をまとめて削除(クリーンアップ) - 段階1の観察継続(数日)→ 段階2(CLB完全廃止 + Nginx集約)へ
- 明日朝5:00 の
cleanMovieImps初回実行を確認
2026-04-27
セッション - 7channel本番のRDS/PHP最適化(cron頻度調整・インデックス整備・テーブル整理・クエリ改善)
やったこと:
- 段階1(EC2-2単体運用)の24時間観察 → 問題なし(CLB 5xx=0、EC2-2 CPU 1.0-1.5%、RDS変化なし)
- 7channel-server のcron
cron.txtを分析 → 毎分11個の処理が走っていることを確認 - EC2-2 / RDS の重い処理を実測(PHP 5.3 / CakePHP / db.t3.micro MySQL 8.4)
/crawl/article3.1秒、movieUploadsClassic2-2.4秒など- RDS統計: Select_scan 75万、Created_tmp_disk_tables 3612 → 改善余地大
- スロークエリログ有効化(parameter group
mysql84でslow_query_log=ON / long_query_time=1、再起動なし) - cron頻度調整(ユーザー実施):
movieUploadsClassic/0,1を5分毎、removeNotNeedMovieUploadsを30分毎に - インデックス追加・整理:
cast_impsにcast_id追加(ユーザー実施)moviesの重複/無価値インデックス4つ削除(id/youtube_code_2/del/tweet_done、ユーザー実施)articles.done_image_scraping追加(ワースト1対策、ユーザー実施)
- imps系テーブル整理:
cast_impsはSELECT皆無の書き捨てテーブルと判明 →Api3Controller::impressionCastのINSERTをコメントアウト(ローカル修正→デプロイ)movie_impsをテーブルswap方式で削減: 1,100万行 → 21,329行(直近3ヶ月)、old_movie_impsに旧データ保管- 自動クリーンアップ用
CronController::cleanMovieImps()を新規実装+0 5 * * *cron追加
- HotMonthMovie 復活(ユーザー cron 編集):
33 4 * * * /api3/updateHotMovies/3を有効化 - スロークエリ ワースト2/3 のソース改善:
- ワースト2
updateCastMovieCount:getAllで1,628行fetchしてPHPcount()していたのをfind('count', recursive=-1)に変更 - ワースト3 (3箇所):
ORDER BY DATEDIFF(?, upload_date) ASCを等価変換でORDER BY upload_date DESCに置換 → upload_date インデックス活用可
- ワースト2
気づき:
7channel-serverは CodeCommit + CodeDeploy 管理。git pushにはAWS_PROFILE=ichirokisanukiが必須(デフォルトプロファイル未設定のため)- Lightsail インスタンスはデフォルト IAM ロールで AWS リソースを叩けないため、IAMユーザーのアクセスキー方式が現実解(前回学習を本番でも踏襲)
- RDS は 割り当て容量で課金。データを消してもストレージ料は変わらない(縮小には新インスタンスへの再構築が必要)
次回やること:
- 数時間後にスロークエリログ再確認(ワースト1〜3 が消えてるか)
- 明日朝5:00 の
cleanMovieImps初回実行を確認 old_movie_impsは数週間〜Lightsail移行時にDROP予定(焦らない)
2026-04-26
セッション - 最終ゴールの共有(CLB廃止・Lightsail集約・RDS廃止)
やったこと:
- ユーザーから長期目標(LB削除、Lightsail 1台で動かす、RDSのデータをLightsailに持っていく)が共有された
- これらは既にROADMAPの段階2/3に含まれているため、ROADMAP変更は不要と確認
- プロジェクトメモリに「7channel本番のAWS構成最終ゴール」として保存(次回以降のセッションで自動的に参照される)
セッション - AWSコスト分析と段階1実行(ALBから1台外す)
やったこと:
- 月$106のAWS請求内訳を分析:
- VPC $22.31(Public IPv4 6個分の課金)
- RDS $20.53、ELB $20.19、EC2 $20.14、Registrar $15、Route53 $5
- EC2 2台に SSH 接続(自分のIP
133.32.128.92/32をSSH SGに一時追加)して構成を把握- PHP 5.3.29 / Apache 2.2.34 / Amazon Linux AMI 2018.03(全部EOL、2014〜2023年でサポート終了)
- CakePHPアプリ。CodeDeploy管理、
/var/www/html配下 - 直近1週間のリクエスト: 30,000〜50,000/日、iOSアプリUA識別: 約700/日、QPS平均1未満
- メモリ: 461MB中185MB使用(buffer抜き)、CPU平均 0.6%
- RDS
kizuna(db.t3.micro MySQL) を分析- 77テーブル、合計2.3GB。最大は
movie_imps(1100万行/1.5GB) - CPU平均 5.7-7.3%、接続数ほぼ常時0(スパイク時58)
- 77テーブル、合計2.3GB。最大は
- Lambda 4個 + EventBridge ルール多数を確認
- 実在Lambda:
CMN_totalHealthCheck(外形監視)、CMN_detachEC2FromELB/CMN_attachToELB/CMN_startEC2(EC2オートヒーリング) - 全部 nodejs8.10(EOL)
- 多数の孤児EventBridgeルール(参照Lambdaが既に削除済み)を発見
- 実在Lambda:
- 段階1実行(ALBから1台外す・低リスク・即リバート可能):
- EC2-1 (
i-0e9fe6c9d4bca23ae) のAppNameタグを削除(CMN_attachToELBの自動再登録対象から外す) - CLB
Channel7から EC2-1 をデタッチ - EC2-1 を停止(Public IPも解放)
- 効果: 約 $7.4/月削減、サービスは EC2-2 で継続稼働中
- EC2-1 (
気づき:
- iOSアプリのDAU 500だが、実トラフィックは Lightsail Nano 1台で余裕で捌ける規模
- 7channel本番サーバーは技術的負債が深刻(PHP/OS/Lambda 全部EOL)。コスト削減は同時にリプレースを促すきっかけにできる
- Lambda+EventBridge での自前オートヒーリングは2019年頃のベストプラクティス、今ならALB+ASGかLightsailで不要
次回やること:
- 1台体制で数日様子見
- 問題なければ段階2(ALB完全廃止 + Nginx集約)へ
- SSHホワイトリストに追加した
133.32.128.92/32を削除(クリーンアップ)
2026-04-25
セッション - 動画追加時のアラートメール復旧
やったこと:
- 動画追加時のSNS経由アラートメールが届かない問題を調査
- cron.log で
AuthorizationError: ...is not authorized to perform: SNS:Publishを確認 - 原因特定: Lightsailインスタンスは AWS 管理アカウント(
385232584017)で動いており、デフォルト付与のAmazonLightsailInstanceRoleには SNS:Publish 権限がない。しかもこのロールは AWS 管理で変更不可 - SNSトピック側にはクロスアカウント許可(
AllowLightsailPublish)が既にあったが、IAM側ロールを変更できないので無効 - 解決策として旧サーバーで使っていた IAM ユーザーのアクセスキーを新サーバーの
~/.aws/credentialsに配置([default]プロファイル、パーミッション 600) notify.send_notificationを手動実行してメール到達を確認
気づき:
- Lightsail インスタンスは「ユーザーアカウント」と「AWS管理アカウント」の二層構造になっている
AmazonLightsailInstanceRoleは AWS 管理リソースなので IAM ポリシーをアタッチできない- Lightsail から AWS リソースを使う場合、IAMユーザーのアクセスキーを置く方式が現実解
2026-04-24
セッション - サブディレクトリ整理とcron復旧
やったこと:
lightsail-claude-avatar/をゴミ箱へ移動(不要のため)7channel-server-v2/を7channel-operationリポジトリに統合- 元々独立リポジトリ(
.git内蔵)だったが、.gitを削除して通常ファイルとして取り込み - GitHub上の独立リポジトリ
7channel-server-v2はそもそも作られていなかったのでアーカイブ不要
- 元々独立リポジトリ(
- 旧サーバーで動いていた
curate.pyの10分おきcronが新サーバーで未稼働だったので復旧*/10 * * * * cd /home/ubuntu/7channel-server-v2 && venv/bin/python curate.py >> cron.log 2>&1
- 「サーバー移管時はcron等の周辺要素も漏れなくチェック」を memory に保存
気づき:
- サーバー移行ではコードのデプロイだけでなく、cron / systemd timer / 常駐デーモンのチェックも必須
セッション - リポジトリ初期化と7channel-server-v2の多目的サーバー移行
やったこと:
git init実行、.gitignore設定(7channel-server/,7channel-ios/, Claude Code ローカル状態).devnotes/運用を導入(DEVLOG.md, WIP.md)- GitHubにprivateリポジトリ(
7channel-operation)を作成、初回commit & push 7channel-server-v2を旧Lightsail(13.230.63.19/ ec2-user)から多目的サーバー(multipurpose-server1/ ubuntu)に移行- 新サーバーに
python3.10-venvインストール、venv作成 - systemdサービス(
7channel-web)を8001ポートで設定・起動 - Nginx に
7ch.ikapps.comのバーチャルホスト設定を追加 - ファイルをSCPで転送、依存パッケージインストール、サービス起動確認(HTTP 200)
- Route53のAレコードを
13.230.63.19に手動更新 - 静的IP
7channel-v2-ip(13.230.63.19)を多目的サーバーにアタッチ deploy.shを新サーバー向けに更新
- 新サーバーに
multi-purpose-lightsail-server1のREADME/CLAUDE.mdを更新してcommit & push
次回やること:
7channel-server-v2/とlightsail-claude-avatar/をこのリポジトリでコミット管理するか方針決め
最近のコミット
- b03c6ce deploy: 2026-06-18 17:00 2026/6/18
- d766a3d deploy: 2026-06-18 13:04 2026/6/18
- 4f24310 .devnotes 更新: 記事日付1970バグ修正・記事一覧レイアウト改善・iOS 8.3.2審査提出を記録 2026/6/18
- 4e91d39 記事日付の1970/01/01バグを修正: APIフォーマットをiOS期待値に合わせる 2026/6/18
- 47ebef1 deploy: 2026-06-18 04:13 2026/6/18
- 5682a8c deploy: 2026-06-17 21:10 2026/6/17
- 326918a deploy: 2026-06-17 21:01 2026/6/17
- 88bf6a2 run_cast_matcher: 多重起動防止ガードの誤検出を修正 2026/6/17
- e239233 cast_candidates.py 追加: 未登録出演者の候補抽出(discover)+既知castの全期間バックフィル(backfill) 2026/6/16
- 7a39e7c deploy: 2026-06-16 16:26 2026/6/16