# RankRadar（Fuzoku Tracker）開発仕様書

## 1. システム概要

風俗求人媒体（ガールズヘブン・バニラ）の順位監視・更新最適化SaaS。
クライアントの求人掲載順位をリアルタイムスクレイピングし、AIが最適な更新タイミングを算出・設定する。

---

## 2. インフラ

| 項目 | 内容 |
|------|------|
| VPS | Windows Server 2022（133.18.137.226） |
| Apache | C:\Apache24\ |
| PHP管理画面（クライアント） | C:\Apache24\htdocs\rankradar\php\client\ |
| Python | C:\apps\ranking_system\ |
| カゴヤDB | lerisa_rankingsystem（共用サーバー） |
| VPS内DB | rankradar_data（VPS MySQL） |
| ドメイン | client.kst.fuzokutracker.com（HTTPS対応済み） |
| SSL | Let's Encrypt（win-acme）次回更新：2026/9/6 |
| phpMyAdmin | http://db.kst.fuzokutracker.com/phpmyadmin/ |
| カゴヤオーナー画面URL | https://lerisa.kir.jp/rankradar/ranking-system/php/owner/ |
| VPSクライアント画面URL | http://133.18.137.226/rankradar/php/client/ |
| スクリーンショット | C:\apps\ranking_system\screenshots\ → /rankradar/screenshots/（Apacheエイリアス） |

---

## 3. デプロイ手順

### PHPの場合（VPS Tera Termから）
```cmd
cd C:\Apache24\htdocs\rankradar
git fetch origin
git merge origin/claude/split-database-connections-hIrzc
git push origin HEAD:main
git reset --hard origin/main
```

### Pythonの場合（VPS Tera Termから）
```cmd
cd C:\apps\ranking_system
git fetch origin
git merge origin/claude/split-database-connections-hIrzc
git push origin master:main
git reset --hard origin/main
```

### カゴヤサーバー（自宅PCコマンドプロンプトから）
```cmd
ssh -p 10022 -i C:\ssh\private.key lerisa@lerisa.kir.jp "cd ~/public_html/rankradar/ranking-system && git pull origin main"
```

### VPSとGitHubが乖離した場合
```cmd
git reset --hard origin/main
```

---

## 4. タスクスケジューラ（VPS）

| 時刻 | タスク名 | スクリプト | 内容 |
|------|---------|-----------|------|
| 23:55 | RankRadar_Backup_Client1 | backup.py 1 | DBバックアップ→FTP転送 |
| 0:01 | RankRadar_GetCount_GH_Client1 | get_update_count_girlsheaven.py 1 | GHアクセス統計/最大更新回数取得 |
| 0:01 | RankRadar_GetCount_Vanilla_Client1 | get_update_count_vanilla.py 1 | バニラ最大更新回数取得 |
| 0:05 | RankRadar_Analyze_Client1 | analyze.py 1 | 前日データ分析・最適更新時刻算出 |
| 0:10 | RankRadar_SetBenry_Client1 | set_benry.py 1 | ベンリーに更新時刻設定 |
| 0:10 | RankRadar_SetVanilla_Client1 | set_vanilla.py 1 | バニラに更新時刻設定（記録上0:12） |
| 0:30 | RankRadar_CompetitorPatterns_Client1 | analyze_competitor_patterns.py 1 | 競合の更新パターン分析（GH専用） |
| 常時 | RankRadar_Monitor_Client1 | monitor.bat経由 | 24時間スクレイピング継続 |
| 常時 | RankRadar_Watchdog_Client1 | watchdog.bat経由 | monitor.py監視・自動再起動 |

※実機の schtasks /query が正。旧記載の05時台スケジュールは廃止済み。

### Watchdog タスク登録コマンド（Tera Termから）
```cmd
schtasks /create /tn "RankRadar_Watchdog_Client1" ^
  /tr "C:\apps\ranking_system\watchdog.bat" ^
  /sc minute /mo 5 ^
  /st 06:05 /et 05:00 /k ^
  /ru SYSTEM /f
```

---

## 5. DB構成

### 5-1. カゴヤDB（lerisa_rankingsystem）

**接続情報**
- ホスト: mysql57s-32.kagoya.net:3306
- ユーザー: lerisa
- パスワード: stance0209P0531#

| テーブル | 重要カラム | 備考 |
|---------|-----------|------|
| `clients` | id, shop_name, email, password | 店舗基本情報。shop_nameはフォールバック用 |
| `media_settings` | client_id, media_name, is_active, login_id, login_pass, monitor_url, target_rank, max_update_count, **shop_name** | 媒体ごとの設定。shop_nameはGH・バニラ別に異なる表記対応 |
| `monitor_settings` | client_id, start_time, end_time, interval_min | 監視設定。**start_time/end_timeはmonitor.pyから参照されない死んだ値。interval_minのみ使用**（24時間稼働） |
| `tool_settings` | client_id, tool_name（benry/shinchakukun）, login_id, login_pass, tool_id, is_active | 更新ツール設定 |
| `excluded_times` | client_id, **media_name**, start_time, end_time（TIME型）| 除外時間帯。日をまたぐ対応済み。media_name追加で媒体別管理 |
| `fixed_update_times` | client_id, **media_name**, update_time（**TIME型・分単位**・例:15:02:00）| 絶対更新時間。3分スロットへの丸め廃止。media_name追加で媒体別管理 |
| `update_timing_settings` | client_id, **media_name**, update_mode（**1=集中型, 2=分散型, 3=アクセス統計連動**） | 更新モード。UNIQUE(client_id, media_name)。翌日から適用 |
| `client_memos` | client_id, memo, updated_at | ダッシュボードのメモ欄 |
| `notices` | id, title, body, publish_at, is_published, target_type（1=全員） | お知らせ |
| `notice_recipients` | notice_id, client_id | 特定クライアント向けお知らせ |
| `faqs` | id, category, question, answer, sort_order, is_published | Q&A |

**新規クライアント登録時（client_edit.php）に自動挿入するSQL**
```sql
INSERT INTO monitor_settings (client_id, start_time, end_time, interval_min)
VALUES (%s, '06:00:00', '05:00:00', 3)
ON DUPLICATE KEY UPDATE start_time=start_time, end_time=end_time, interval_min=interval_min
```

### 5-2. VPS内DB（rankradar_data）

**接続情報**
- ホスト: 127.0.0.1:3306
- ユーザー: root
- パスワード: stance0209P0531#

| テーブル | 重要カラム | 備考 |
|---------|-----------|------|
| `scraping_results` | client_id, media_name, shop_name, \`rank\`（**予約語・必ずバッククォート**）, page_no, fetched_at | 3分毎の順位データ |
| `analysis_results` | client_id, media_name, analyzed_date, best_hhmm, summary_comment, **update_mode**, created_at | 日次解析結果。update_modeは翌日のグラフポイントに適用 |
| `analysis_suggestions` | client_id, media_name, suggest_time（VARCHAR・1時間1行）, reason_text, suggested_at, slot_score | **1行1時間**（旧: JSON配列1行は廃止） |
| `gh_access_stats` | client_id, stat_date, hour_slot（HH:MM・30分刻み48件）, total_access, shop_access | GHアクセス統計 |
| `update_histories` | client_id, media_name, executed_at, status（1=成功/0=失敗） | ベンリー/バニラへの更新設定履歴 |
| `error_logs` | client_id, media_name, error_type, error_message, occurred_at | スクレイピングエラーログ |
| `competitor_update_patterns` | client_id, media_name, shop_name, window_days（7/30）, is_tool, pattern_type（daily/weekday/irregular）, confidence, update_count, bump_times_json, computed_at | 競合更新パターン（GH専用）。analyze_competitor_patterns.pyが日次生成（0:30） |

**マイグレーション**
```sql
-- analysis_resultsにupdate_modeカラム追加（適用済み）
ALTER TABLE analysis_results
  ADD COLUMN update_mode TINYINT DEFAULT 1
  COMMENT '1:集中型 2:分散型 3:アクセス統計連動'
  AFTER summary_comment;
```

**推奨インデックス（timeline.php高速化）**
```sql
ALTER TABLE scraping_results ADD INDEX idx_client_media_date (client_id, media_name, fetched_at);
```

---

## 6. PHP DB接続・設定ファイル

### php/client/config/db.php
```php
get_db()       // カゴヤDB（lerisa_rankingsystem）
get_local_db() // VPS内DB（rankradar_data）
```

### php/client/config/auth.php
- `require_client_login()` → 未認証時 `header('Location: /php/client/login.php')` にリダイレクト
- `client_logout()` → セッション破棄後 `/php/client/login.php` にリダイレクト
- セッション変数: `$_SESSION['client_id']`、`$_SESSION['client_name']`

### Python DB接続（python/config/database.py）
```python
execute_query(sql, params, local=True)   # VPS内DB（rankradar_data）
execute_query(sql, params, local=False)  # カゴヤDB（lerisa_rankingsystem）
execute_update(sql, params, local=True)  # VPS内DB（INSERT/UPDATE）
```

---

## 7. PHP クライアント管理画面（詳細仕様）

### 共通仕様
- フレームワーク: Bootstrap 5.3.3 + FontAwesome 6.5.0
- サイドバー幅: 240px（CSS変数 --sidebar-width）
- メインカラー: #1D9E75
- サイドバー背景: #1F3864（紺）
- レスポンシブ: 992px未満でサイドバーがハンバーガーメニュー化
- サイドバーの「各種設定」はアコーディオン（settings.php表示時は常に展開）
- 設定サブメニュー: 媒体設定・更新ツール設定・モニタリング時間設定・更新タイミング設定・上位目標設定

### dashboard.php
**データソース**
- カゴヤDB: clients, media_settings(target_rank), tool_settings, notices, client_memos
- VPS内DB: scraping_results, analysis_results, update_histories

**表示内容**
1. サマリーカード（3枚）: GH上位N位以内時間（達成率%付）・バニラ1ページ目時間（達成率%付）・未読お知らせ数
2. AI解析コメント：GH・バニラの媒体別2枠（各媒体のanalysis_resultsをanalyzed_date降順1件で取得、媒体名・日付ラベル付き表示）
3. 媒体サマリーカード: GH（本日/同一プラン総数/前日比）、バニラ（本日/7日平均/前日比）
4. 更新実行一覧テーブル（直近1ヶ月、5件ページング）
5. お知らせ一覧（5件ページング）
6. メモ（自由記述・POST保存）

**達成率計算**
```php
$gh_achievement  = round($gh_today      / 1440 * 100, 1);
$van_achievement = round($vanilla_today / 1440 * 100, 1);
```

**滞在時間計算**（dashboardカードも generate_summary も COUNT×3 に統一）
- GH: `COUNT(*) * 3` WHERE rank <= target_rank
- バニラ: `COUNT(*) * 3` WHERE rank <= 20（バニラの1ページ＝20店舗）

**前日比**
- 本日と同じ経過時間（前日00:00〜本日現在の同時刻まで）で比較

### timeline.php
**URL パラメータ**
- `period`: 7d / 30d（デフォルト 7d）
- `media`: girlsheaven / vanilla（デフォルト girlsheaven）
- `hdate`: YYYY-MM-DD（時間帯別グラフの日付）

**グラフカード（紺色背景デザイン）**
- card-header背景: #1a2a3a、文字: #ffffff
- card-body背景: #1a2a3a
- 高さ: 370px
- Chart.js 4.x（`getPixelForValue`使用・`getPixelForIndex`は旧API）

**update_mode 日付オフセット（重要）**
```php
// analyzed_dateが'2026-07-16'のupdate_modeは翌日'2026-07-17'のポイントに適用
$next_date = date('Y-m-d', strtotime($row['date'] . ' +1 day'));
$update_modes_by_date[$next_date] = (int) $row['update_mode'];
```

**ポイント色（getModeColor）**
```javascript
function getModeColor(dateLabel) {
    const mode = updateModesByDate[dateLabel] || 0;
    if (mode === 1) return '#e53935';  // 集中型（赤）
    if (mode === 2) return '#1565C0';  // 分散型（青）
    if (mode === 3) return '#E65100';  // アクセス統計連動（オレンジ）
    return '#2e7d32';                  // デフォルト（緑）
}
```

**達成率ラベル（afterDatasetsDraw）**
- ポイント上に `XX.X%` を表示（bold 12px、getModeColor色）
- pointRadius: 4、pointHoverRadius: 6（ボーダーなし）

**凡例HTML**（グラフカード内）
- 集中型更新（#e53935）、分散型更新（#1565C0）、アクセス統計連動更新（#E65100）
- 各spanにtitle属性でツールチップ説明あり

**グラフタイトル横の説明テキスト**
```php
// GHの場合
"※%は上位{$target_rank}位以内達成率（24時間中の割合）"
// バニラの場合
"※%は1ページ目達成率（24時間中の割合）"
```

**クリック→モーダル**
- ポイントクリック → api/timeline_shop_stay.php へfetch → 店舗別滞在時間モーダル表示

**時間帯別グラフ（インライン埋め込み）**
- timeline.php下部に hourly グラフ埋め込み（hdate日付ナビ付き）
- 独立ページは hourly.php

**統計サマリーカード（グラフ下）**
- 1日平均滞在時間（平均達成率%付）、最長滞在日、最短滞在日

**日別詳細カード**
- 日付ごとに滞在時間（達成率%付）＋AI解析コメント（折りたたみ）

### hourly.php
**URL パラメータ**
- `media`: girlsheaven / vanilla
- `hdate`: YYYY-MM-DD
- `interval`: 表示間隔（分）

**グラフ仕様（紺色背景 #1a2942）**
- 幅: `max(2400px, timeLabels.length * 18)px`（固定width canvas）
- ドラッグスクロール対応（mousedown/mousemove）
- 全店舗表示（自店: #1D9E75 太線・前面、競合: パレット15色 細線）
- 店舗フィルタードロップダウン

**グラフ上の機能**
- beforeDraw: GH目標順位以内の緑帯（1位〜targetRank位）
- beforeDraw: 更新時刻（suggest_time）に赤い破線 + 「N回目」テキスト（500ms点滅）
- クリック: 自店ポイントのみ → スクリーンショットモーダル表示
  - URL構築: `/rankradar/screenshots/girlsheaven/GH-YYYYMMDD-HHMM.jpg`

**表示間隔の選択肢**
- base_interval × 1/2/4/6 + 30分 + 60分の組み合わせ（monitor_settings.interval_minから計算）

**スクレイピング履歴テーブル**
- 500件取得、40件ページング
- 自店舗行: 緑ハイライト (#1D9E75 15%)
- 圏外(rank=0): 「圏外」オレンジ表示
- 店舗フィルター対応

### history.php
**ページング仕様**
- 1ページ = 5日分（page=1: 今日〜4日前、page=2: 5〜9日前...）
- 媒体フィルター: すべて/GH/バニラ

**タイムライン表示（B案デザイン）**
- 日付別 → 媒体別カード
- 曜日ごとに背景色（日:赤系、土:青系）

**update_modeバッジ**（analysis_resultsから取得）
- mode=1: 集中型（#d1ecff / #185FA5）
- mode=2: 分散型（#d4f5e9 / #0F6E56）
- mode=3: アクセス統計連動（#ffe8cc / #b45309）

**suggest_time表示仕様**
- 時間昇順（ksort）で表示
- fixed_timesに一致 → 「絶対更新時間対応」バッジ（#1D9E75・太字）・reason_text非表示
- それ以外 → reason_textを表示

**データ構造（analysis_suggestions）**
```
旧（廃止）: suggest_times（JSON配列）で1行
現行: suggest_time（VARCHAR）で時間ごとに1行・reason_textカラムあり
```

### scraping_log.php
**ドットグリッド**
- 1マス: 22px × 22px（border-radius: 4px）
- 成功: #8B5CF6（紫）、エラー: #E24B4A（赤）、データなし: #eaeff5（グレー）
- 横方向: interval_min刻みのbucket
- 縦方向: 0〜23時

**スクリーンショットリンク**
- glob: `C:\apps\ranking_system\screenshots\girlsheaven\GH-YYYYMMDD-HH??.jpg`（バケット範囲内）
- 複数ある場合はファイル名降順で最新1件のみ返す
- リンク先: `http://133.18.137.226/rankradar/screenshots/` 配下

**エラーログ表示**
- format_error_message() でユーザー向けメッセージに変換
  - timeout → 「接続がタイムアウトしました」
  - connection refused → 「サイトに接続できませんでした」
  - URLをサニタイズ（qzin→バニラ、girlsheaven→ガールズヘブン）

### competitor_analysis.php（GH専用・競合の更新パターン分析）
**ページ名**: 競合分析レポート（GH）。バニラは対象外（媒体セレクタから削除済み）。

**データソース**
- `competitor_update_patterns`（VPS内DB rankradar_data）を日次バッチ `analyze_competitor_patterns.py`（0:30）で生成
- 順位の上位遷移（1〜2位入り）を更新イベントとみなし、規則スロットからツール判定/曜日別/確度を算出

**表示カード（1店舗1カード）**
- 「1日N回」ピル、時刻マーカー、曜日別グリッド
- フィルタ: すべて / ツール判定 / 不定期
- ページング: 10件

**廃止された旧機能**
- 穴場時間帯 Top5（競合更新が少ない30分枠）
- 競合店別マトリクス（時刻×曜日）

### settings.php
**5タブ構成**
- `?tab=media`: 媒体設定（GH・バニラのログイン情報・監視URL・is_active）
- `?tab=tool`: 更新ツール設定（benry/shinchakukun・ログイン情報・tool_id）
- `?tab=monitor`: モニタリング時間設定（interval_min: 最短3分・開始/終了の入力欄は削除済み）
- `?tab=timing`: 更新タイミング設定（GH/バニラ媒体別サブ切替 `tmedia` パラメータ・update_mode選択・除外時間帯・絶対更新時間）
- `?tab=target`: 上位目標設定（target_rank: min=ceil(total/3), max=total）

**媒体設定の特別動作**
- 保存後に api/get_shop_name.php を呼び出し店名を自動取得
- 店名取得済みの場合は入力欄をreadonly化（再登録不可）

**モニタリング時間設定の制約**
- 開始・終了時刻の入力欄は画面から削除済み（monitor.pyはstart/endを参照しないため）
- 最短3分（フロント・サーバー両方バリデーション）
- サマリー表示: 24時間 ／ 約N回（1440 / interval_min）

**更新タイミング設定の注意**
- **媒体別管理**: GH/バニラのサブ切替（`?tab=timing&tmedia=girlsheaven|vanilla`）
- update_mode変更は翌日から適用（ダイアログ表示）
- 除外時間帯: 日をまたぐ設定対応（23:00〜翌04:00 等）
- 絶対更新時間: TIME型で分単位保存（例: 15:02:00）
- バニラのmode3（アクセス統計連動）はgh_access_statsを代理指標として流用

**上位目標設定**
- 同プラン競合店舗一覧（最新スクレイピング日のDISTINCT shop_name）も表示
- min_rank = ceil(total_shops / 3)、max_rank = total_shops
- 競合店舗一覧の下に「理論上のフェアシェア（順位N÷総数）」表を表示（追加DBクエリなし・$total_shops_count/$target_rank_val流用、目標順位行をハイライト）

### notices.php
- 公開済み・公開日時以前のお知らせ全件
- target_type=1（全員宛）またはnotice_recipientsに自クライアントが含まれるもの

### faq.php
- is_published=1のFAQをcategory・sort_order順
- カテゴリ別アコーディオン
- JS検索（キーワードフィルタ・カテゴリブロック非表示対応）

### api/timeline_shop_stay.php
```
GET ?date=YYYY-MM-DD&media=girlsheaven|vanilla
Response: [{"shop_name":"...", "stay_minutes":N}, ...]
```
- GH: rank_limit = target_rank（media_settings.target_rankから取得）
- バニラ: rank_limit = 20（バニラの1ページ＝20店舗）
- `COUNT(*) * 3 AS stay_minutes` で計算

---

## 8. Python スクリプト詳細仕様

### ファイル構成
```
python/
├── analysis/
│   └── analyze.py              # 解析エンジン（メイン）
├── scraping/
│   ├── monitor.py              # 監視ループ
│   ├── scrape_girlsheaven.py
│   ├── scrape_vanilla.py
│   └── watchdog.py
├── tools/
│   ├── get_access_girlsheaven.py   # GHアクセス統計取得
│   ├── get_shop_name.py            # 店名自動取得
│   ├── set_benry.py                # ベンリー自動設定
│   ├── set_vanilla.py              # バニラ自動設定
│   ├── backup.py
│   ├── get_update_count_girlsheaven.py
│   └── get_update_count_vanilla.py
└── config/
    ├── database.py             # DB接続
    └── encryption.py
```

### monitor.py
```python
INTERVAL_JITTER = 10        # ±10秒のランダム揺らぎ
WAIT_OUTSIDE_HOURS = 60     # DB設定取得失敗時のリトライ間隔（秒）※監視時間外とは無関係
```
- 定刻スロット実行（00分・03分・06分...の3分間隔）
- `_get_slot_time()` でスロット時刻計算、`_next_slot_wait()` で次スロットまでの待機秒数算出
- `ThreadPoolExecutor(max_workers=2)` でGH・バニラを並列スクレイピング
- **24時間稼働（無限ループ）**。自動終了ロジックなし。プロセス制御はタスクスケジューラ側に依存
- 00:00〜04:00の間、1/80の確率で `get_access_girlsheaven.py` を自動実行（1日1回）
- start_time/end_timeはDBから取得せず（interval_minのみ使用）

### watchdog.py
```python
TIMEOUT_MINUTES = 10    # monitor.pyが10分間応答なしでタイムアウト
CHECK_INTERVAL  = 300   # 5分ごとにチェック
```
- `wmic process where commandline like '%monitor.py%'` でPID検出
- タイムアウト検出時: `schtasks /run /tn RankRadar_Monitor_Client{client_id}` で再起動
- エラーログをerror_logs（local=True）に記録

### monitor.bat / watchdog.bat
- 単純なラッパー: `@echo off` + `python.exe <script_path> <client_id>` のみ
- ウィンドウ表示ありで起動（監視しやすいため）

### backup.py
- mysqldump → `C:\backup\rankradar_00001_YYYYMMDD.sql`（5桁ゼロパディング）
- FTP転送: lerisa.kir.jp、ユーザー: lerisa..rankradar
- FTPルートディレクトリ: client_id（5桁ゼロパディング）
- 30日超えのファイルを自動削除
- ローカルファイルはFTP転送後に削除

### get_update_count_vanilla.py
- Playwright でhttps://qzin.jp/entry/ にアクセス
- 削除ボタンの数をカウント → max_update_count として media_settings に保存（local=False）

### get_access_girlsheaven.py
- Playwright + playwright_stealth
- ログイン: https://manager.girlsheaven-job.net/ → joblogページ
- Highchartsデータ抽出: `\{\s*y:\s*(\d+)\s*,\s*marker:\s*\{\s*enabled:\s*false\s*\}\}` の正規表現
- 前半48件 = total_access、後半48件 = shop_access（30分刻み）
- gh_access_stats にUPSERT（local=True）、stat_date = date.today()

### set_benry.py
- Selenium + ChromeDriverManager（selenium-manager自動DL）
- URL: https://mrvenrey.jp/#/admin
- analysis_suggestions から最新日付の suggest_time を取得（media=girlsheaven）
- 操作フロー: ログイン → ID切替 → コンテンツ更新 → GH編集 → 時刻指定更新タブ → 止める → すべて削除 → 時刻入力 → 設定保存 → 更新開始
- update_histories に記録（status=1成功/0失敗）

### set_vanilla.py
- Playwright
- URL: https://qzin.jp/entry/
- analysis_suggestions から最新日付の suggest_time を取得（media=vanilla）
- 操作フロー: ログイン → 店舗上位表示設定 → 全削除 → 時間セレクト + 分セレクト → 追加ボタン → 保存
- 05時台は自動除外（コード上で維持）
- update_histories に記録

---

## 9. analyze.py アルゴリズム詳細

### 全体フロー
1. カゴヤDBから設定取得（max_update_count、target_rank）。update_mode・excluded_times・fixed_update_times は**媒体ループ内**で媒体別に取得
2. VPS内DBからスクレイピングデータ取得（直近1日分）
3. gh_access_statsから実アクセスデータ取得（直近7日平均）
4. 2ステップで更新時刻候補を算出
5. analysis_suggestionsに保存（1行1時間・reason_text付）
6. analysis_resultsに保存（best_hhmm・summary_comment・**update_mode**）

### 2ステップアルゴリズム

**ステップ①: アクセスデータで30分枠ごとの更新回数を配分**

モードA（集中型 update_mode=1）:
```python
# 平均アクセスの50%未満の枠をスキップ
if access[slot] < mean_access * 0.5:
    skip
```

モードB（分散型 update_mode=2）:
```python
# 全30分枠を対象（スキップなし）
# gh_access_statsのアクセス割合で更新回数を配分
```

モードC（アクセス統計連動 update_mode=3）:
```python
# gh_access_statsのみで算出
# 競合スコアリングなし
```

**ステップ②: 各30分枠内で最適な3分スロット選出**

GH競合スコア（rival_score）:
- 競合更新直後スコア: 競合が5位以上改善した直後のスロット
- 競合静穏スコア: 競合が動いていない静かなスロット

バニラ競合スコア:
- 競合更新直後スコア + 競合静穏スコア + 1ページ目滞在スコア

弱点スコア（weakness_score、mode=2のみ）:
```python
weakness_scores: dict[int, float] = {}
if update_mode == 2:
    own_df_w = media_df[media_df["is_own_shop"]].copy()
    if not own_df_w.empty:
        own_df_w["in_target"] = (own_df_w["rank"] > 0) & (own_df_w["rank"] <= target_rank)
        for s30 in range(0, 1440, 30):
            in_frame = own_df_w[(own_df_w["slot"] >= s30) & (own_df_w["slot"] < s30 + 30)]
            weakness_scores[s30] = (1.0 - float(in_frame["in_target"].mean())) if len(in_frame) > 0 else 0.5
```

合成スコア（mode=2）:
```python
ws = weakness_scores.get(s30, 0.0) if update_mode == 2 else 0.0
score = rival_score.get(s, 0.0) * 0.5 + ws * 0.5
```

### 重要な設計判断
- **PEAK_HOURS廃止**: 固定ピーク時間帯は廃止。gh_access_statsの実データのみ使用
- **絶対更新時間**: 3分スロットに丸めず設定時刻そのまま（例: 15:02）を候補に追加
- **除外時間帯**: 日をまたぐ設定対応（23時→翌4時の場合 `h >= 23 or h <= 4`）
- **バニラの05時台自動除外**: コード上で維持（現在のset_vanilla.py実行は0:10だがコード内除外は継続）
- **gh_access_statsがない場合**: 直近7日平均を試み、なければ解析スキップ
- **競合更新判定**: `(prev_rank - rank) >= 5`（単純な順位変動と実際の更新を区別）

### Claude API連携
- モデル: `claude-haiku-4-5-20251001`
- 各提案時間ごとに個別の理由文を生成（80文字程度）
- 形式: 「自店状況：〇〇。競合動向：〇〇。結論：〇〇。」
- 競合店3店以下→店名明記、4店以上→「主要競合店」と表現
- ネガティブ表現禁止（「効果が限定的」「優先度が低い」等）

### save_analysis_results シグネチャ
```python
def save_analysis_results(
    client_id: int,
    media_name: str,
    analyzed_date: str,
    best_hhmm: str,
    summary_comment: str,
    update_mode: int = 1  # ← 追加済み
) -> None:
```

---

## 10. スクレイピング仕様

### scrape_girlsheaven.py
- Playwright + playwright_stealth（ステルス設定）
- RETRY_COUNT=1、RETRY_WAIT=15秒
- URL: monitor_url に `/?op=newc` を付加（正規化）
- 年齢確認: `.over18confirm-yes` をクリック
- もっと見るボタン: `a.nextBtn`（最大MAX_MORE_CLICKS=20回）
- 店舗行セレクタ: `.shopBox.rankS.yellowColor`
- 順位: HTMLコメント `<!-- page:rank -->` から `<!-- \d+:(\d+) -->` の正規表現で取得
- 店名取得: media_settings（fallback: clients）
- スクリーンショット: `C:\apps\ranking_system\screenshots\girlsheaven\GH-YYYYMMDD-HHMM.jpg`（JPEG quality=50）
- scraping_results（local=True）+ CSVにも保存
- slot_timeを fetched_at として使用

### scrape_vanilla.py
- Playwright + playwright_stealth
- MAX_SCAN_PAGES=2、RETRY_COUNT=1、RETRY_WAIT=15秒
- 店舗行セレクタ: `li.searchResult-shop-item`
- 店舗名: `h3.shop-name a` のテキスト
- ページャー: `.mod-pager .pager-item a`
- 2ページまで読み込み（1ページ目で総ページ数確認後にmin(total, MAX_SCAN_PAGES)）
- 2ページ目はスクロール→ページネーションクリック（BOT対策）
- 自店舗名を含む/含まれる店名を部分一致で検索
- 圏外(rank=0)でも all_results に追加して記録
- スクリーンショット: `C:\apps\ranking_system\screenshots\vanilla\VLA-YYYYMMDD-HHMM.jpg`
- scraping_results（local=True）に全店舗保存、CSVは自店舗のみ
- sub_area（`p.shop-info` の `|` 区切り先頭部分）も取得

### スクリーンショットアクセス
- Webアクセス: http://133.18.137.226/rankradar/screenshots/girlsheaven/GH-YYYYMMDD-HHMM.jpg
- hourly.phpでのURL構築:
  ```javascript
  const url = '/rankradar/screenshots/' + (isGH ? 'girlsheaven' : 'vanilla') + '/' + filename;
  ```

---

## 11. 手動実行コマンド（VPS Tera Term）

```cmd
REM analyze.py
C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\analysis\analyze.py 1

REM GHアクセス統計
C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\tools\get_access_girlsheaven.py 1

REM GH店名取得
C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\tools\get_shop_name.py 1 girlsheaven

REM バニラ店名取得
C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\tools\get_shop_name.py 1 vanilla

REM monitor.py再起動
taskkill /f /im python.exe
schtasks /run /tn "RankRadar_Monitor_Client1"
```

---

## 12. 更新モード仕様

| モード | 内容 | analyze.pyの動作 |
|--------|------|-----------------|
| 1：集中型 | アクセスピーク時間帯に集中 | 平均アクセスの50%未満の30分枠をスキップ |
| 2：分散型 | 全時間帯対象・弱い時間帯強化 | 全30分枠対象 + weakness_score × 0.5 + rival_score × 0.5 |
| 3：アクセス統計連動 | GHアクセス統計のみで算出 | スコアリングなし・アクセス割合のみで配分 |

**重要な日付ルール**
- update_modeの変更は翌日から適用
- timeline.phpのグラフ: analyzed_dateのupdate_modeは翌日のポイントに適用（+1日オフセット）
- history.phpのバッジ: analyzed_dateそのものの日付に表示

---

## 13. 重要な注意事項・既知の問題

### SQL予約語
- `rank` はMySQLの予約語。必ずバッククォートで囲む: `` `rank` ``

### Chart.js バージョン
- Chart.js 4.x を使用
- `getPixelForValue` を使用（`getPixelForIndex` は3.x以前のAPIで動作しない）

### 店名の取得先
```sql
-- GH・バニラ両方ともmedia_settingsから取得（clientsはフォールバック）
SELECT shop_name FROM media_settings
WHERE client_id = %s AND media_name = %s AND is_active = 1 LIMIT 1
```

### モニタリング時間
- **24時間稼働**（0〜23時すべてスクレイピング。メンテナンス時間帯なし）
- monitor_settings の start_time/end_time は monitor.py から参照されない死んだ値。interval_min のみ使用
- 達成率の分母は1440分（滞在分 / 1440 × 100）

### SSL証明書
- DNS検証方式（手動）→ 次回更新時にTXTレコード追加が必要
- メールアドレス: lerisaashop@icloud.com
- 更新コマンド: `cd C:\win-acme → wacs.exe --renew --force`

### 既知のバグ
- settings.phpでtab=targetにアクセスすると、以前は更新タイミング設定が表示された（現在は修正済み・target tabとして独立）

### WindowsUpdateの停止
- VPSのWindowsUpdateは無効化済み（Disable設定）

### analysis_suggestionsの変更（廃止 → 現行）
```
旧（廃止）: suggest_times（JSON配列）で1行
現行: suggest_time（VARCHAR）で時間ごとに1行・reason_textカラムあり
```
set_benry.py・set_vanilla.pyは新構造に対応済み。

### analyze.pyの滞在時間計算
- 5分以上のgapはスキップ（連続スクレイピングの欠損対策）

---

## 14. タスクスケジューラ方式（重要・本番安定性に直結）

### 必須設定：BootTrigger + SYSTEM + RestartOnFailure

VPSでPythonスクリプトを安定稼働させるには以下3点が必須。

| 設定項目 | 正しい値 | 誤った値（禁止） |
|---------|---------|----------------|
| トリガー | BootTrigger（OS起動時） | LogonTrigger（ユーザーログオン時） |
| 実行ユーザー | SYSTEM | 特定ユーザー（RDPセッションに依存） |
| 失敗時の再起動 | 有効（RestartInterval/RestartCount設定） | なし |

**LogonTriggerの致命的な問題：**  
RDP（リモートデスクトップ）を切断すると `logoff` ではなく `disconnect` になる場合でも、
Windowsによってセッション終了時にタスクが停止されることがある。
BootTrigger + SYSTEM（セッション #0）であれば RDP の接続・切断に一切影響されない。

### 2026-08-03〜08-08 本番停止事象

**原因の連鎖：**
1. タスクスケジューラが **LogonTrigger** で設定されていたため、RDPセッション終了時に monitor.py が停止
2. `watchdog.py` も同じくLogonTriggerで登録されており、同時に停止 → 再起動が機能せず
3. `watchdog.py` の `_find_monitor_pids()` が `wmic` を使用しているが、Windows Server 2022 では `wmic` が非推奨・動作不安定のため PID 検索が空振り → `kill_monitor()` が「見つからない」と警告するだけで終了
4. `monitor.py` が `sys.executable` で子プロセスを起動していたため、AppData 側の Python で起動した場合は venv パッケージが使えずスクレイピングが即失敗

**対応済み（2026-08-08）：**
- タスクスケジューラを BootTrigger + SYSTEM + RestartOnFailure に再登録すること（VPS Tera Termから要手動対応）
- `monitor.py` の子プロセス起動を `sys.executable` → `PYTHON_EXE`（venv固定パス）に修正 ✅
- `monitor.py` に多重起動防止ロック機構を追加 ✅

### タスクスケジューラ再登録コマンド（VPS Tera Term）

```
:: Monitor（毎日06:00、BootTrigger＋SYSTEMは schtasks ではなくタスクスケジューラGUIで設定推奨）
:: BootTrigger は XML インポートで登録する（schtasks /create の /sc onstart が代替）

:: 以下は /sc onstart 版（BootTrigger相当）
schtasks /create /tn "RankRadar_Monitor_Client1" ^
  /tr "C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\scraping\monitor.py 1" ^
  /sc onstart /delay 0000:30 ^
  /ru SYSTEM /f

:: Watchdog（06:05〜05:00、5分ごと）
schtasks /create /tn "RankRadar_Watchdog_Client1" ^
  /tr "C:\apps\ranking_system\watchdog.bat" ^
  /sc minute /mo 5 ^
  /st 06:05 /et 05:00 /k ^
  /ru SYSTEM /f
```

> **注意:** BootTrigger を正確に設定するには タスクスケジューラGUI の「コンピューターの起動時」トリガーを使うか、XML定義ファイルをインポートすること。`/sc onstart` は近似だが起動遅延制御が限定的。

### 失敗時の再起動設定（タスクスケジューラGUI）

タスクのプロパティ → 設定タブ：
- 「タスクが失敗した場合の再起動の間隔」: 1分
- 「再起動試行回数」: 3

---

## 15. スクレイピング停止アラート

### 概要

`python/monitoring/check_scraping_alert.py` がスクレイピングの停止を検知してGmail SMTPでメール通知する。

### ファイル構成

| ファイル | 説明 | Git管理 |
|---------|------|---------|
| `python/monitoring/check_scraping_alert.py` | 監視スクリプト本体 | ✅ あり |
| `python/monitoring/alert_config.example.py` | 設定テンプレート（コピーして使用） | ✅ あり |
| `python/monitoring/alert_config.py` | 実際の設定（パスワード含む） | ❌ .gitignore済み |
| `python/monitoring/alert_state.json` | 通知状態の保存（送信済み判定） | ❌ .gitignore済み |

### セットアップ手順（VPS初回）

```
:: 1. 設定ファイルをコピー
copy C:\apps\ranking_system\python\monitoring\alert_config.example.py ^
     C:\apps\ranking_system\python\monitoring\alert_config.py

:: 2. alert_config.py をエディタで編集（SMTP_APP_PASSWORD, MAIL_TO 等を設定）

:: 3. pymysql がインストールされているか確認（Playwright venv と別に必要な場合あり）
C:\apps\ranking_system\venv\Scripts\pip.exe install pymysql

:: 4. テスト送信
C:\apps\ranking_system\venv\Scripts\python.exe ^
  C:\apps\ranking_system\python\monitoring\check_scraping_alert.py --test
```

### alert_config.py の主要設定

```python
SMTP_HOST        = "smtp.gmail.com"
SMTP_PORT        = 587
SMTP_USER        = "your-gmail@gmail.com"
SMTP_APP_PASSWORD = "xxxx xxxx xxxx xxxx"   # Gmailアプリパスワード
MAIL_FROM        = "your-gmail@gmail.com"
MAIL_TO          = ["alert-recipient@example.com"]
STALE_MINUTES    = 15                        # 何分更新がなければアラート

TARGETS = [
    (1, "girlsheaven"),
    (1, "vanilla"),
]
```

### 通知ロジック

- `scraping_results.fetched_at` の MAX を監視対象ごとに確認
- `STALE_MINUTES` 分以上更新がなければ即座にアラートメール送信（`alerting=True`）
- その後 1時間ごとに再送（継続通知）
- スクレイピングが再開されると復旧メール送信（`alerting=False`に戻す）
- DB接続失敗もアラート扱い

### タスクスケジューラ登録（VPS Tera Term）

```
schtasks /create /tn "RankRadar_ScrapingAlert" ^
  /tr "C:\apps\ranking_system\venv\Scripts\python.exe C:\apps\ranking_system\python\monitoring\check_scraping_alert.py" ^
  /sc minute /mo 5 ^
  /ru SYSTEM /f
```

---

## 16. monitor.py 多重起動防止ロック機構

### 背景

タスクスケジューラの誤設定や watchdog の再起動処理により、monitor.py が二重起動するケースがあった。
特に venv 版と AppData 版が「ほぼ同時に」起動すると、チェック→書き込みの2ステップロックは突破されてしまう。

### 実装（OS レベル原子的排他）

`os.O_CREAT | os.O_EXCL | os.O_WRONLY` フラグでロックファイルを作成。  
このフラグ組み合わせは OS カーネルが原子的に処理するため、2プロセスが同時に `os.open()` を呼んでも片方だけが成功する。

**ロックファイルパス：** `C:\apps\ranking_system\monitor_{client_id}.lock`

```
起動
 ↓
os.O_CREAT|O_EXCL でロックファイル作成を試みる
 ├─ 成功 → PID を書き込む → read-back で確認 → 正常起動
 └─ FileExistsError
     ├─ ロックファイルの PID を読む
     │   ├─ PID が生存中 → 「既に稼働中」ログ → sys.exit(0)
     │   └─ PID が死亡 or 読み取り失敗（空ファイル等）
     │       ├─ リトライ上限未達 → ロックファイル削除 → 短時間待機 → 再試行
     │       └─ リトライ上限超過 → RuntimeError
```

**read-back 検証：** `O_EXCL` 成功後に自分の PID を書き込み、直後に読み直して一致確認。
極めて稀なエッジケース（ファイルシステム障害等）で不一致が起きた場合は `sys.exit(0)` で安全に終了。

### プロセス生存確認（psutil 不要）

```python
def _pid_is_alive(pid: int) -> bool:
    result = subprocess.run(
        ["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"],
        capture_output=True, text=True, timeout=5,
    )
    return str(pid) in result.stdout
```

`wmic` の代わりに `tasklist` を使用（Windows Server 2022 での `wmic` 非推奨対応）。

### ロック解放

- `atexit.register(_release_lock)` で正常終了・例外終了どちらでも解放
- `finally: _release_lock()` を `main()` に追加（二重解放は無害）
- PID 一致確認後のみ削除（他プロセスが奪取したロックを誤って削除しない）

---

## 現在の進捗

### 完了済み
- [x] GH・バニラのスクレイピング（3分間隔・定刻スロット実行）
- [x] analyze.py 2ステップアルゴリズム実装
- [x] gh_access_statsをピークスコアに反映（PEAK_HOURS廃止）
- [x] 集中型・分散型・アクセス統計連動モード実装
- [x] 分散型（mode=2）に自店弱点スコア反映（weakness_score × 0.5）
- [x] 除外時間帯の日をまたぐ対応
- [x] 絶対更新時間の分単位対応（TIME型・3分スロット丸め廃止）
- [x] Claude APIによる理由文の自動生成
- [x] 店名自動取得（GH・バニラ・media_settingsから取得）
- [x] get_access_girlsheaven.py（Highchartsデータ取得）
- [x] Q&Aページ（クライアント・オーナー）
- [x] サイドバーのアコーディオン化
- [x] history.phpの時間昇順・絶対更新時間バッジ・update_modeバッジ
- [x] timeline.phpの紺色背景デザイン
- [x] timeline.phpの達成率%ラベル（afterDatasetsDraw）
- [x] timeline.phpのupdate_mode色分け・凡例・+1日オフセット
- [x] dashboard.phpの達成率%表示
- [x] auth.phpのリダイレクトURL修正（/php/client/login.php）
- [x] analysis_resultsにupdate_modeカラム追加
- [x] settings.phpにtarget（上位目標設定）タブ追加
- [x] AI解析コメントを媒体別2枠・COUNT×3統一・前日比を同時刻比較に
- [x] バニラの順位しきい値を表示層で20に統一（1ページ＝20店舗）
- [x] 競合の更新パターン分析（GH専用・トップ遷移検出・ツール判定/曜日別/確度・フィルタ/ページング）
- [x] competitor_update_patterns の日次バッチを0:30に登録
- [x] 更新タイミング設定の媒体別化（3テーブルmedia_name追加・settings.php媒体別UI・analyze.py媒体別読込）
- [x] 更新モードカードのデザイン刷新
- [x] 上位目標設定にフェアシェア理論値表を追加
- [x] モニタリングサマリーを24時間(1440分)整合・start/end参照を除去
- [x] 表示名称「ガールズヘブン」→「GH」統一、競合分析レポート(GH)にリネーム
- [x] monitor.py の子プロセス起動を sys.executable → PYTHON_EXE（venv固定）に修正
- [x] monitor.py に多重起動防止ロック機構追加（O_CREAT|O_EXCL + read-back検証）
- [x] スクレイピング停止アラート（check_scraping_alert.py + Gmail SMTP + 状態管理）
- [x] GH店名取得（get_shop_name.py）ログイン処理修正（<a>タグ対応・expect_navigation + form.submit fallback）
- [x] デバッグスクリプト追加（debug_shop_name.py・debug_login_form.py）
- [x] .gitignore に alert_config.py・alert_state.json を追加

### PENDING（未確認・未完了）
- [ ] hourly.phpのメンテナンス時間帯背景色表示確認
- [ ] set_benry.py・set_vanilla.pyの動作確認
- [ ] オーナー画面faq.phpのカゴヤデプロイ確認
