<?php
// index.php

function getLatestPhpMtime(string $dir): ?int {
    $pattern = rtrim($dir, '/'). '/*.php';
    $files = glob($pattern);
    if (!$files) {
        return null;
    }

    $latest = 0;
    foreach ($files as $file) {
        $t = filemtime($file);
        if ($t > $latest) {
            $latest = $t;
        }
    }
    return $latest > 0 ? $latest : null;
}

// ▼ 先に初期化
$updaterank   = '';
$updatelog    = '';
$updateproglam = '';

// 解析システム更新時刻（ディレクトリ直下 .php の最終更新）
$latestPhpTime = getLatestPhpMtime(__DIR__);
if ($latestPhpTime) {
    $updateproglam = date('Y/m/d H:i', $latestPhpTime);
}

// ログ取得バッチ更新時刻（人狼オンラインログ取得バッチ・早朝以外10分毎実行）
$tsFile = __DIR__ . '/zinro_batch.log';
if (is_file($tsFile)) {
    $ts = filemtime($tsFile);
    if ($ts) {
        $updatelog = date('Y/m/d H:i', $ts);
    }
}

// キャッシュバッチ更新時刻（高速化用・早朝１回実行）
$tsFile = __DIR__ . '/batchcache.txt';
if (is_file($tsFile)) {
    $ts = filemtime($tsFile);
    if ($ts) {
        $updaterank = date('Y/m/d H:i', $ts);
    }
}

?><!DOCTYPE html>
<html lang="ja">
<head>
  <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-24X1HW4ZBE"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-24X1HW4ZBE');
</script>
<meta charset="utf-8">
<title>＊’サプリの小道具 - 人狼オンライン戦績解析＆Wiki</title>
<link rel="stylesheet" href="style.css?v=<?= filemtime(__DIR__ . '/style.css') ?>">
<script src="app.js?v=<?= filemtime(__DIR__ . '/app.js') ?>" defer></script>
<script src="seasonal-petals.js?v=<?= filemtime(__DIR__ . '/seasonal-petals.js') ?>" defer></script>
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
</head>
<body>
<div class="wrapper">
  <header class="header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
    <div style="flex-shrink:0;">
      <div class="title-main">＊’サプリの小道具</div>
      <div class="title-sub">人狼オンライン戦績解析＆Wiki</div>
    </div>

    <!-- ヘッダー右：ログインエリア -->
    <div id="headerAuth" style="font-size:.8rem;flex-shrink:0;text-align:right;">

      <!-- 非ログイン時 -->
      <div id="headerLoginArea">
        <button id="headerLoginToggleBtn" class="btn btn-primary" style="font-size:.75rem;padding:4px 10px;">ログイン</button>
        <!-- ミニログインフォーム（展開式） -->
        <div id="headerLoginForm" style="display:none;position:absolute;right:8px;top:56px;
          background:var(--card-bg,#1a2035);border:1px solid var(--border,#2a3555);
          border-radius:12px;padding:12px 14px;z-index:200;min-width:260px;box-shadow:0 4px 24px #0006;">
          <div style="font-weight:600;margin-bottom:8px;">ログイン</div>
          <label style="font-size:.75rem;">名前（必須）</label>
          <input type="text" id="loginNameInput" placeholder="例：サプリ" required autocomplete="username" style="width:100%;margin-bottom:6px;">
          <label style="font-size:.75rem;">トリップキー</label>
          <input type="text" id="loginTripKeyInput" placeholder="例：abcdEFGH" required autocomplete="off" spellcheck="false" style="width:100%;margin-bottom:8px;">
          <label style="display:flex;align-items:center;gap:6px;margin:0 0 9px;font-size:.75rem;cursor:pointer;text-align:left;">
            <input type="checkbox" id="loginRememberInput" style="width:auto;margin:0;">
            HN/PASSをこの端末に保存する
          </label>
          <div style="display:flex;gap:6px;align-items:center;">
            <button id="loginBtn" class="btn btn-primary" style="font-size:.8rem;">ログイン</button>
            <button id="headerLoginCancelBtn" class="btn btn-ghost" style="font-size:.8rem;">キャンセル</button>
          </div>
          <div id="loginMsg" style="margin-top:6px;font-size:.75rem;color:var(--accent);min-height:1em;"></div>
          <div style="margin-top:6px;font-size:.7rem;color:var(--muted);line-height:1.4;">
            ※名前＋トリップキーで生成したトリップが戦績DBに存在すればログイン成功<br>
            ※## 先頭で固定トリップ（例：##key）
          </div>
        </div>
      </div>

      <!-- ログイン済み（初期非表示） -->
      <div id="headerUserArea" style="display:none;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;">
        <span id="headerUserName" style="color:var(--muted);font-size:.78rem;"></span>
        <button id="logoutBtn" class="btn btn-ghost" style="font-size:.75rem;padding:3px 10px;">ログアウト</button>
      </div>

    </div>
  </header>


<main class="site-content">
<nav class="tabs">
  <button class="tab-btn active" data-tab="tab-about">概要</button>
  <button class="tab-btn" data-tab="tab-chrome">Chrome拡張</button>
  <button class="tab-btn" data-tab="tab-personal">個人戦績</button>
  <button class="tab-btn" data-tab="tab-monthly">ランキング</button>
  <button class="tab-btn" data-tab="tab-chains">変名図</button>
  <button class="tab-btn" data-tab="tab-rm-stats">RM集計</button>
  <button class="tab-btn" data-tab="tab-ai-trial">AI解析</button>
  <button class="tab-btn" data-tab="tab-bbs">掲示板</button>
  <button class="tab-btn" data-tab="tab-command">コマンド</button>
  <button class="tab-btn" data-tab="tab-link">リンク</button>
  <button class="tab-btn" data-tab="tab-login">プロフィール</button>
</nav>

<!-- AI解析 -->
<section id="tab-ai-trial" class="tab-panel" style="display:none">
  <nav class="ai-subtabs" aria-label="AI解析の種類">
    <button class="ai-subtab is-active" type="button" data-ai-subtab="player">プレイヤー解析</button>
    <button class="ai-subtab" type="button" data-ai-subtab="game">ゲームログ解析</button>
  </nav>
  <div id="aiSubPlayer">
  <div class="card" style="margin-bottom:12px;">
    <h2>AI解析</h2>
    <p style="font-size:.82rem;color:var(--muted);line-height:1.7;">
      過去1年以内に参加し、保存済みゲームチャットを十分に確認できるトリップを、最終参加日時の新しい順に解析しています。未解析者は10分ごとの戦績更新後に順次解析し、解析済みのプレイヤーも月が替わって新しいゲームログがあれば再評価します。以前の評価は現在の解析のすぐ下にある履歴から確認できます。
      レーダーはAIの主観点ではなく、発言量・盤面語・質問・被質問への応答・他者名への言及・根拠接続語を機械集計した客観プロフィールです。
      文章はゲーム中の発言と前後の文脈をQwen 3が読んだ参考解析です。勝敗・役職・過去戦績は使わず、人格や実力全般を断定するものではありません。
      全件解析後に追加する会話影響力スコアは、周囲の反応や議論の流れを動かした度合いを0〜1000で表します。偏差値は現在の解析済みプレイヤー全体を母集団とし、平均50・標準偏差10で算出します。
      さらに、異なる10ゲームID以上を確認できたプレイヤーは、開始前チャットから見える「人となり」と、終了後チャットから見える「感想戦」の印象を追加します。RMを担当したトリップには、RM時だけの発言から村の雰囲気づくり、参加者対応、ルール説明、開始判断や運営方針を見る「RM評価」も表示します。待ち時間、立て直し、RMの管理発言を考慮し、発言が少ない場合は無理に評価を広げません。
    </p>
    <div id="aiTrialMeta" style="font-size:.75rem;color:var(--muted);"></div>
    <section class="ai-score-distribution" aria-labelledby="aiScoreDistributionTitle">
      <div class="ai-score-distribution-head">
        <h3 id="aiScoreDistributionTitle">会話影響力スコアと偏差値のプレイヤー分布</h3>
        <span id="aiScoreDistributionSummary">分布を読み込み中…</span>
      </div>
      <div id="aiScoreDistribution" class="ai-score-distribution-chart" aria-live="polite"></div>
    </section>
    <details class="ai-trial-method"><summary>レーダーの計算基準</summary>
      1戦50発言、盤面語率35%、質問率25%、被質問応答率100%、他者名言及率30%、根拠語率25%をそれぞれ1000として換算し、上限を1000にしています。質問は「名前を含む疑問文」または直後に本人が話した疑問文、回答はその後5発言以内の本人発言として機械判定します。優劣の点数ではなく、発言スタイルの形を見るための指標です。
    </details>
    <div class="ai-trial-controls">
      <input id="aiTrialSearch" type="search" placeholder="名前・トリップ・解析文を検索">
      <select id="aiTrialMinGames" aria-label="確認戦数で絞り込み">
        <option value="0">確認戦数：すべて</option><option value="3">3戦以上</option><option value="5">5戦以上</option><option value="10">10戦</option>
      </select>
      <select id="aiTrialSort" aria-label="並べ替え">
        <option value="latest">最終参加が新しい順</option><option value="influence">会話影響力スコア順</option><option value="answers">被質問応答率順</option><option value="talk">発言数順</option><option value="questions">質問率順</option><option value="reasons">根拠語率順</option>
      </select>
      <span id="aiTrialFilteredCount"></span>
    </div>
  </div>
  <div id="aiTrialList" class="ai-trial-grid">
    <div class="card">解析結果を読み込み中…</div>
  </div>
  <div id="aiTrialPager" class="ai-trial-pager"></div>
  </div>
  <div id="aiSubGame" style="display:none">
    <div class="card ai-game-intro">
      <h2>ゲームログ解析</h2>
      <p>戦績処理で配役名が確定した新着ゲームを定期的に確認し、保存されたゲーム中のログからAIが試合単位でまとめた参考解析です。総合・勝利貢献、場への影響、推理・情報整理、説得・協働、局面判断の優先順で5つの視点を切り替えられます。MVPは勝敗だけでなく、推理、情報整理、判断、陣営への貢献をログから総合して選びます。</p>
      <div id="aiGameMeta">解析結果を読み込み中…</div>
      <div class="ai-game-controls"><input id="aiGameSearch" type="search" placeholder="ゲームID・村名・プレイヤー名・トリップ・解析文を検索"><span id="aiGameFilteredCount"></span></div>
    </div>
    <div id="aiGameList" class="ai-game-grid"><div class="card">解析結果を読み込み中…</div></div>
  </div>
</section>

<!-- 概要 -->
<section id="tab-about" class="tab-panel" style="display:block">
  <div class="layout">
    <div class="card">

      <h3>このサイトについて</h3>
      <p style="font-size:.85rem;color:var(--muted);">
        人狼オンラインの歴史上、秋雨前線さんのに続く２番目に登場した戦績解析ツールでしたが、時々休止を繰り返してました。最近いちめろが時々停止するらしいので、補完できるよう、長く続けられるよう頑張ります。よろしくお願いします。<br>
      </p>

      <h3>更新履歴</h3>
      <p style="font-size:.85rem;color:var(--muted);">
        <b>2026-03-25</b> ログインまわりを修正。<br>
        <b>2026-01-03</b> 戦績表示のChrome拡張版、v1.0.0公開開始。<br>
        <b>2026-01-03</b> 戦績データをSQLiteからPostgreSQLでの処理に変更完了。<br>
        <b>2026-01-01</b> 戦績表示のChrome拡張版、実験公開開始。<br>
        <b>2025-11</b> 一念発起してチャッピー（ChatGPT）に戦績ツールをリメイクしてもらって再開。<br>
        <b>2023-04</b> 休止してたけど、再公開。<br>
        <b>2021-02</b> サプリの小道具/戦績解析機を作り、ログ収集と一部公開を開始。<br>
      </p>

    </div>

    <div class="card">
      <h3>主な機能</h3>
      <p style="font-size:.85rem;color:var(--muted);">
      <b>個人戦績</b>：名前 or トリップから、配役別・役職別の勝率や同村者ランキングを表示<br>
      <b>ランキング</b>：月間 / 年間 / 通算の参戦数・勝率・発言密度などを集計<br>
      <b>プロフィール / Wiki / コメント</b>：トリップごとの人物紹介・メモ・感想など<br>
    </p>

      <h3>使い方のヒント</h3>
      <p style="font-size:.85rem;color:var(--muted);">
        名前とトリップどちらでも検索できます。ランキングの名前/トリップをクリックすると、そのまま個人戦績タブへ飛べます。<br>
      </p>

      <h3>余談</h3>
      <p style="font-size:.85rem;color:var(--muted);">
        サプリは主にPHPで開発してきた技術者ですが、Javascriptでの動的なUIを作るのが苦手でした。このサイトの登場から4年半、画面遷移を伴うダサいUIで運営してました。チャッピーに出会ったのは2023年春前でした。多くの方とAI時代の幕開けと感動を共有したいです。<br>
        現在、様々なプロジェクトを並行して進めています。告知をさせて頂くこと多々あると思いますが、応援して頂ければ幸いです。<br><br>
<div style="max-width: 600px; font-size:.85rem;">

  <div style="display:flex; justify-content:space-between; gap:8px; margin-bottom:4px;">
    <span style="color:#9ca3af;">個人サイト：</span>
    <a href="https://dokasen.com/mit/" style="margin-left:auto; text-align:right; font-size:.85rem;">
      https://dokasen.com/mit/
    </a>
  </div>

  <div style="display:flex; justify-content:space-between; gap:8px; margin-bottom:4px;">
    <span style="color:#9ca3af;">X（Twitter）イキリ垢：</span>
    <a href="https://x.com/mitdok/" style="margin-left:auto; text-align:right; font-size:.85rem;">
      https://x.com/mitdok/
    </a>
  </div>

  <div style="display:flex; justify-content:space-between; gap:8px; margin-bottom:4px;">
    <span style="color:#9ca3af;">X（Twitter）交流垢：</span>
    <a href="https://x.com/mitnaitoh/" style="margin-left:auto; text-align:right; font-size:.85rem;">
      https://x.com/mitnaitoh/
    </a>
  </div>

  <div style="display:flex; justify-content:space-between; gap:8px; margin-bottom:4px;">
    <span style="color:#9ca3af;">X（Twitter）地震うさぎbot垢：</span>
    <a href="https://x.com/usagieq/" style="margin-left:auto; text-align:right; font-size:.85rem;">
      https://x.com/usagieq/
    </a>
  </div>

</div>


        <img src="supple.jpg" alt="サプリ27歳写真" style="width:120px;margin-top:8px;border-radius:8px;"><span style="font-size:.85rem;color:#9ca3af;">賛否両論あるサプリ27歳の時の写真</span><br>
        <img src="eq.png" alt="地震うさぎ震度地図" style="width:300px;margin-top:8px;border-radius:8px;"><br><span style="font-size:.85rem;color:#9ca3af;">2011年から運営してる地震うさぎbot画像例（<a href="https://x.com/usagieq/">@usagieq</a>）</span><br>
      </p>

    </div>
  </div>
</section>

<!-- 個人戦績 -->
<section id="tab-personal" class="tab-panel" style="display:none">
  <div class="layout">
    <div class="card">
 <div style="display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;">
   <h2 style="margin:0;">個人戦績検索</h2>
   <span id="personalStatsUpdatedAt" style="margin-left:auto;text-align:right;font-size:.75rem;color:var(--muted);white-space:nowrap;">戦績データ更新：— ／ 確認：—</span>
 </div>

<input type="hidden" id="key_type">
<input type="hidden" id="key_value">


<div class="row">
  <label>名前</label>
  <input id="personalNameQuery" type="text" placeholder="例：サプリ">
  <button id="personalNameSearchBtn" class="btn btn-primary" type="button">
    名前で検索
  </button>
</div>

<div class="row" style="margin-top:8px;">
  <label>トリップ</label>
  <input id="personalTripQuery" type="text" placeholder="例: ◆SuppleLohk / abcd1234 / ◆ABCDEFGHIJKL">
  <button id="personalTripSearchBtn" class="btn btn-primary" type="button">
    トリップで検索
  </button>
</div>


<!-- ▼ 個人戦績：検索中ローディング表示（app.js が使う） -->
<div id="personalLoading"
     style="display:none;
            margin:6px 0 8px;
            padding:6px 10px;
            border-radius:999px;
            border:1px solid var(--accent);
            background:var(--accent-soft);
            color:var(--accent);
            font-size:.8rem;
            font-weight:600;">
  検索中です… 戦績が多い場合は表示に時間がかかることがあります。
</div>

      <div style="height:8px"></div>
      <div id="personalSummary" style="font-size:.8rem;color:var(--muted);"></div>

      <div id="personalHistory" style="font-size:.75rem;color:var(--muted);margin-top:6px;"></div>
      <div id="personalQueryLabel" style="font-size:.8rem;color:var(--muted);margin-bottom:4px;"></div>

      <!-- ▼ どこから来ても表示する汎用戻るボタン -->
      <div id="personalBackGeneral" style="display:none;margin-bottom:4px;"></div>
      <!-- ★ RMから来たときだけ出す「戻る」ボタン -->
      <div id="personalBackFromRm" style="display:none;margin-bottom:4px;"></div>

      <!-- ★ 検索結果ブロック -->
      <div id="personalResultMain" style="display:none;">

        <!-- ★ GitHub風カレンダーの描画先（app.js が #personalCalendar を使う） -->
        <div id="personalCalendar"></div>

        <!-- ★ AI解析（トリップ検索時、評価データがある場合のみ表示） -->
        <div id="personalAiEval" style="display:none;">
          <h3>AI解析</h3>
          <div id="personalAiEvalBody"></div>
        </div>

        <!-- ★ 変名の系譜図（トリップ検索時、系譜データがある場合のみ表示） -->
        <div id="personalChainFig" style="display:none;">
          <h3>変名の系譜</h3>
          <div id="personalChainFigBody" style="overflow-x:auto;"></div>
        </div>

        <div class="reverse-headline-slot" id="personalReverseHeadline"></div>

<div class="table-wrap">
  <div id="personalReverseInline" style="font-size:.8rem;"></div>
</div>

        <h3>配役別勝敗</h3>

        <!-- サブタブナビ -->
        <div style="display:flex;gap:0;margin-bottom:6px;border-bottom:2px solid var(--border);">
          <button id="haiyakuTabNormal" onclick="switchHaiyakuTab('normal')"
            style="padding:4px 14px;font-size:.8rem;font-weight:600;cursor:pointer;border:none;
                   background:none;color:var(--accent);border-bottom:2px solid var(--accent);
                   margin-bottom:-2px;">通常配役</button>
          <button id="haiyakuTabChaos" onclick="switchHaiyakuTab('chaos')"
            style="padding:4px 14px;font-size:.8rem;font-weight:600;cursor:pointer;border:none;
                   background:none;color:var(--muted);border-bottom:2px solid transparent;
                   margin-bottom:-2px;">カオスその他</button>
        </div>

        <!-- 元テーブル（app.js が書き込む・非表示のまま） -->
        <table id="personalHaiyakuTable" style="display:none;">
          <thead>
            <tr>
              <th>配役</th>
              <th class="num">戦数</th>
              <th class="num">勝ち</th>
              <th class="num">勝率</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>

        <!-- 通常配役サブテーブル -->
        <div id="haiyakuPanelNormal" class="table-wrap">
          <table id="personalHaiyakuNormalTable">
            <thead>
              <tr>
                <th>配役</th>
                <th class="num">戦数</th>
                <th class="num">勝ち</th>
                <th class="num">勝率</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>

        <!-- カオスその他サブテーブル -->
        <div id="haiyakuPanelChaos" class="table-wrap" style="display:none;">
          <table id="personalHaiyakuChaosTable">
            <thead>
              <tr>
                <th>配役</th>
                <th class="num">戦数</th>
                <th class="num">勝ち</th>
                <th class="num">勝率</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>

        <h3>役職別勝敗</h3>
        <div class="table-wrap">
          <table id="personalRoleTable">
            <thead>
              <tr>
                <th>役職</th>
                <th class="num">戦数</th>
                <th class="num">勝ち</th>
                <th class="num">勝率</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>

        <!-- ▼ プレイヤー情報 / Wiki / コメント（横幅フル） -->
        <div class="card" style="grid-column: 1 / -1;">
          <h2>プレイヤー情報・Wiki・コメント</h2>

          <!-- ログイン状態表示 -->
          <div id="personalLoginInfo"
               style="margin-bottom:8px;font-size:.8rem;color:var(--muted);">
            ログイン状態を確認中…
          </div>

          <!-- ─── 自己紹介 ───────────────────────────────────────────── -->
          <div id="profileSection" style="display:none;">
            <h3>自己紹介</h3>
            <div id="profileView" style="font-size:.8rem;color:var(--muted);margin-bottom:4px;"></div>
          </div>

          <!-- ─── Wiki ─────────────────────────────────────────────────── -->
          <h3>Wiki</h3>

          <div id="wikiBox">
            <!-- 表示モード -->
            <div id="wikiView"
                 style="font-size:.8rem;border:1px solid var(--border);border-radius:12px;
                        padding:6px 8px;min-height:40px;background:#101426;">

              <div id="wikiViewBody">
                まだWikiは登録されていません。
              </div>

              <!-- ログイン済みのときだけ表示（JS が制御） -->
              <div id="wikiEditToggleArea" style="display:none;margin-top:6px;">
                <button id="wikiEditToggleBtn" class="btn btn-primary" type="button">Wikiを編集</button>
              </div>
            </div>

            <!-- 編集モード -->
            <div id="wikiEdit"
                 style="display:none;font-size:.8rem;border:1px solid var(--border);border-radius:12px;
                        padding:6px 8px;min-height:40px;background:#101426;">

              <div style="font-size:.75rem;color:var(--muted);margin-bottom:4px;">
                ※Wikiはログイン中のユーザーなら誰でも編集できます。改行・URLが自動でリンクになります。
              </div>
              <textarea id="wikiBody" rows="8" style="width:100%;"></textarea>
              <div style="display:flex;gap:6px;margin-top:4px;">
                <button id="wikiSaveBtn"      class="btn btn-primary" type="button" style="display:none;">保存</button>
                <button id="wikiEditCancelBtn" class="btn btn-ghost"  type="button" style="display:none;">キャンセル</button>
              </div>
              <div id="wikiSaveMsg" style="font-size:.75rem;color:var(--muted);margin-top:4px;"></div>
            </div>
          </div>

          <!-- ─── コメント ──────────────────────────────────────────────── -->
          <h3>コメント</h3>
          <div id="commentList" style="font-size:.8rem;margin-bottom:6px;color:var(--muted);">
            コメントはまだありません。
          </div>

          <!-- ログイン済みのときのみ表示 -->
          <div id="commentForm" style="display:none;margin-top:4px;">
            <div class="comment-head" style="margin-bottom:4px;">
              <div style="font-size:.8rem;">投稿者: <b id="comment-author-name">（未ログイン）</b></div>
            </div>
            <label>コメント</label>
            <textarea id="commentBody" rows="4" placeholder="コメントを書く…" style="width:100%;"></textarea>
            <div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap;align-items:center;">
              <button id="commentSendBtn" class="btn btn-primary" type="button">送信</button>
              <span id="commentSendMsg" style="font-size:.75rem;color:var(--muted);"></span>
            </div>
            <div style="margin-top:4px;font-size:.75rem;color:var(--muted);">
              ※ログイン中のトリップがコメントの編集履歴に残ります。
            </div>
          </div>

          <!-- ログアウト時のヒント -->
          <div id="profileLoginHint"
               style="font-size:.75rem;color:var(--muted);margin-top:8px;display:none;">
            <a href="#" class="go-login-tab-link">「プロフィール」タブ</a>からログインすると、Wiki・コメントを編集できます。
          </div>
        </div>

      </div><!-- /personalResultMain -->
    </div>



    <div class="card">
      <h2>説明</h2>
      <p style="font-size:.8rem;color:var(--muted);">
        ・名前またはトリップで検索します。レート・自己紹介・トリップ変遷はトリップ検索でのみ表示されます。<br>
        ・ランキングの名前/トリップをクリックしても、ここに飛んできます。<br>
      </p>
      <h3>⚠️ 割れ酉マークについて</h3>
      <p style="font-size:.8rem;color:var(--muted);">
        ・このサイトとChrome拡張（戦績解析）で表示される <b>⚠️</b> は「割れ酉（われとり）」の印です。<br>
        ・<b>同じトリップが20人以上の異なる名前で使われた実績がある</b>トリップに付きます（毎朝の集計で更新）。<br>
        ・複数人で共有されている酉のほか、<b>意図的に酉を公開したり、複数の名前で使い回している有名プレイヤー</b>も該当します。<br>
        ・不正や成りすましを断定するものではありません。判断の参考情報としてご利用ください。<br>
      </p>
    </div>

    <div class="card" id="personalResultSide" style="display:none;">
      <h3>参戦ログ</h3>
      <div id="personalRecent" style="font-size:.8rem;"></div>

      <h3>参戦村ランキング</h3>
      <div class="table-wrap">
        <table id="personalVillageTable">
          <thead>
            <tr><th>村名</th><th class="num">戦数</th></tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>

      <h3>同村数ランキング</h3>
      <div class="table-wrap">
        <table id="personalPartnerTable">
          <thead>
            <tr><th>名前/トリップ</th><th class="num">同村</th></tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>

  </div>
</section>



  <!-- プロフィール -->
  <section id="tab-login" class="tab-panel" style="display:none">
    <div class="layout">

      <!-- 非ログイン時 -->
      <div class="card" id="profileNotLoggedIn">
        <h2>プロフィール</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ヘッダー右上の「ログイン」ボタンからトリップキーでログインすると、
          プロフィールの登録・編集ができます。
        </p>
        <p style="font-size:.8rem;color:var(--muted);">
          ・ログイン後、個人戦績タブで自分のトリップを検索するとWikiやコメントも編集できます<br>
          ・Wikiはログイン中のユーザーなら誰でも編集できます<br>
          ・コメントはログイン中のユーザーなら誰でも投稿できます<br>
          ・自己紹介の編集は自分のトリップでのログインのみ有効です
        </p>
      </div>

      <!-- ログイン済み時 -->
      <div class="card" id="profileLoggedIn" style="display:none;">
        <h2>プロフィール編集</h2>
        <div id="profileLoggedInUser" style="font-size:.85rem;color:var(--muted);margin-bottom:12px;"></div>

        <label>自己紹介 <span style="font-size:.75rem;color:var(--muted);">（500文字以内）</span></label>
        <textarea id="profileSelfIntro" rows="4" style="width:100%;margin-bottom:8px;"></textarea>

        <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px;">
          <div style="flex:1 1 160px;">
            <label>X（Twitter）ID <span style="font-size:.75rem;color:var(--muted);">（@なしでも可）</span></label>
            <input type="text" id="profileXId" placeholder="例: mitdok" style="width:100%;">
          </div>
          <div style="flex:1 1 160px;">
            <label>X表示名（任意）</label>
            <input type="text" id="profileXDisplay" placeholder="リンクの表示名" style="width:100%;">
          </div>
        </div>

        <button id="profileSaveBtn" class="btn btn-primary">プロフィールを保存</button>
        <div id="profileEditNotice" style="margin-top:6px;font-size:.78rem;color:var(--muted);">
          保存すると、個人戦績タブで自分のトリップを検索した際に表示されます。
        </div>
      </div>

      <!-- 使い方カード（常時表示） -->
      <div class="card">
        <h2>使い方</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ① ヘッダー右上「ログイン」からトリップキーでログイン<br>
          ② このタブで自己紹介・X IDを登録して「保存」<br>
          ③ 個人戦績タブで自分のトリップを検索 → Wiki・コメント・自己紹介を確認・編集<br>
          <br>
          ・Wikiはログイン中のユーザーなら誰でも編集できます<br>
          ・コメントはログイン中のユーザーなら誰でも投稿できます<br>
          ・自己紹介の編集は自分のトリップでのログインのみ有効です
        </p>
      </div>

    </div>
  </section>



  <!-- ランキング -->
<section id="tab-monthly" class="tab-panel" style="display:none">
  <div class="layout">
    <div class="card">
      <div style="display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;">
        <h2 style="margin:0;">ランキング</h2>
        <span id="rankingStatsUpdatedAt" style="margin-left:auto;text-align:right;font-size:.75rem;color:var(--muted);white-space:nowrap;">ランキング集計：— ／ 戦績確認：—</span>
      </div>
      <p style="font-size:.8rem;color:var(--muted);">
        集計種別を選んで表示してください。レートは全期間のトリップ別レーティングです。
      </p>

      <div id="monthlyLoading"
           style="
             display:none;
             margin:6px 0 8px;
             padding:6px 10px;
             border-radius:999px;
             border:1px solid var(--accent);
             background:var(--accent-soft);
             color:var(--accent);
             font-size:.8rem;
             font-weight:600;
           ">
        集計中です… ゲーム数が多い期間は表示に時間がかかる場合があります。
      </div>

<div style="display:flex;flex-direction:column;gap:10px;">

  <!-- ★ 対象期間（スコープ） -->
  <div class="setting-box">
    <label>対象期間</label>
    <select id="monthlyScope">
      <option value="all">全期間</option>
      <option value="year">年間</option>
      <option value="month">月間</option>
    </select>
    <div id="monthlyMonthBox">
      <label>対象月</label>
      <select id="monthlyYm"></select>
    </div>
    <div id="monthlyYearBox" style="display:none;">
      <label>対象年</label>
      <select id="monthlyYear"></select>
    </div>
  </div>

  <!-- ★ 集計種別 -->
  <div class="setting-box">
    <label>集計種別</label>
    <select id="monthlyKind">
      <option value="win_rate">勝率</option>
      <option value="career">活動期間</option>
      <option value="games">参戦数</option>
      <option value="haiyaku_player">配役別</option>
      <option value="rating">レート</option>
      <option value="vote_acc">投票精度</option>
      <option value="talk">発言密度</option>
      <option value="village">村名別</option>
      <option value="rm">RM別</option>
      <option value="tsuiho">追放</option>
    </select>
  </div>

  <!-- ★ 配役（配役別プレイヤー選択時のみ表示） -->
  <div class="setting-box" id="monthlyHaiyakuBox" style="display:none;">
    <label>配役</label>
    <select id="monthlyHaiyaku"></select>
  </div>

  <!-- ★ 検索種別（名前 / トリップ）※常時表示の必須選択 -->
  <div class="setting-box" id="monthlyKeyTypeBox">
    <label>検索種別</label>
    <select id="monthlyKeyType">
      <option value="trip">トリップ別</option>
      <option value="name">名前別</option>
    </select>
  </div>

  <!-- ★ トリップなしの扱い（名前別集計時のみ有効） -->
  <div class="setting-box" id="monthlyNoTripBox">
    <label>トリップなし</label>
    <select id="monthlyNoTrip">
      <option value="0">含める</option>
      <option value="1">除外する</option>
    </select>
  </div>

  <!-- ★ 最低集計数（戦数・件数での絞り込み） -->
  <div class="setting-box">
    <label>最低集計数</label>
    <select id="monthlyMinCount">
      <option value="0">制限なし</option>
      <option value="100">100以上</option>
      <option value="300">300以上</option>
      <option value="500">500以上</option>
      <option value="1000">1000以上</option>
    </select>
  </div>

  <!-- ★ 表示ボタン（独立） -->
  <div>
    <button id="monthlyLoadBtn" class="btn btn-primary">検索＆表示</button>
  </div>
</div>


      <div style="height:6px"></div>
      <div class="table-wrap" id="monthlyTableWrap" style="display:none;">
        <table id="monthlyTable">
          <thead>
            <tr>
              <th>#</th>
              <th id="monthlyLabelTh">項目</th>
              <th class="num" id="monthlyValueTh">件数</th>
              <th class="num" id="monthlyWinsTh">勝ち</th>
              <th class="num" id="monthlyRateTh">勝率</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
      <div style="margin-top:4px;font-size:.75rem;color:var(--muted);" id="monthlyPager"></div>
    </div>

    <div class="card">
      <h2>説明</h2>
      <p style="font-size:.8rem;color:var(--muted);">
        ・対象期間に「開始したゲーム」をもとに集計したランキングです。<br>
        ・勝率／参戦数は検索種別（名前別／トリップ別）を切り替えられます。<br>
        ・村名別／RM別／追放は件数のみを表示します（名前ベース）。<br>
        ・活動期間は初登場日から最終登場日までの経過日数です（期間指定時はその期間内で計算）。名前別は同名の別人が混ざるため、トリップ別が実態に近くなります。<br>
        ・「トリップなし：除外する」は、トリップ未設定の参加記録を名前別集計から除きます（トリップ別では常に除外）。<br>
        ・レートは全期間のトリップ別レーティング上位を表示します（期間指定はありません）。<br>
        ・投票精度は「村側だったときの投票が人狼に当たっていた率」です（ログ取得済みゲームの範囲・最低30票、全期間のみ）。<br>
        ・最低集計数を入れると、戦数・件数がその値以上のものだけに絞り込みます。<br>
        ・配役別は、選んだ配役のゲームだけを対象にしたプレイヤー戦績です（初回表示は集計に時間がかかることがあります）。<br>
        ・100件単位で「もっと見る」が押せます（対応する集計種別のみ）。<br>
      </p>
    </div>
  </div>
</section>


  <!-- RM向け multi trip -->

  <!-- リンク -->
  <section id="tab-link" class="tab-panel" style="display:none">
    <div class="layout">
      <div class="card">
        <p style="font-size:.75rem;color:var(--muted);margin:0 0 12px;line-height:1.6;">
          リンクの右に赤い「更新」マークが付いているサイトは、1日1回の自動巡回で前回から内容の変化を検出したものです。<br>
          トップページだけでなく、そのサイト内を2階層先まで見て判定しています。マークは検出から7日間表示されます。<br>
          灰色の「閉鎖」マークは、現在は閲覧できないと分かったうえで記録として残しているリンクです。
        </p>
        <div id="linkwatchRunMeta" class="linkwatch-run-meta">巡回実績を読み込み中…</div>
        <details class="linkwatch-history">
          <summary>更新確認・巡回履歴を見る</summary>
          <div id="linkwatchHistory">履歴を読み込み中…</div>
        </details>
        <h2>公式</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ・<a href="https://zinro.net/" target="_blank" rel="noopener">人狼オンライン 公式サイト（https://zinro.net/）</a><br>
          ・<a href="https://x.com/jinronline" target="_blank" rel="noopener">人狼オンライン 公式Xアカウント（@jinronline）</a><br>
        </p>

        <h2>戦績ツール（非公式）</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ・<a href="https://aksm128.github.io/zinroLogAnalyzer-Pages/index.html" target="_blank" rel="noopener">秋雨の物置（By秋雨前線さん）</a><span class="lw-archived" style="display:inline-block;margin-left:6px;padding:1px 6px;border-radius:4px;background:#64748b;color:#fff;font-size:11px;font-weight:700;vertical-align:middle;white-space:nowrap" title="現在は閲覧できません。記録として残しています。">閉鎖</span><br>
          ・<a href="https://dokasen.com/zinro/" target="_blank" rel="noopener">サプリの小道具（Byサプリ）</a><br>
          ・<a href="https://olivier-zinro.fc2.net/" target="_blank" rel="noopener">いちごめろんぱんの開発日記（Byいちごめろんぱんさん）</a><br>
        </p>

        <h2>wiki／サイト</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ・<a href="https://zinroonline.wiki.fc2.com/" target="_blank" rel="noopener">くろうさDays2nd（Byくろうさぎさん）</a><br>
          ・<a href="https://zinrojinro.wiki.fc2.com/wiki/" target="_blank" rel="noopener">人狼onlinewiki（Byルアーさん）</a><br>
          ・<a href="https://hanyayuraku.wiki.fc2.com/wiki/" target="_blank" rel="noopener">halfたこ焼の最新版人狼ウィキ（Byhalfたこ焼きさん）</a><br>
          ・<a href="https://mattarimura.amebaownd.com/" target="_blank" rel="noopener">まったり村</a><br>
          ・<a href="https://sites.google.com/view/online387/home" target="_blank" rel="noopener">Online幼稚園（Byさやなさん）</a><br>
          ・<a href="https://magicowl.wiki.fc2.com/wiki/" target="_blank" rel="noopener">人狼online発展部</a><br>
          ・<a href="https://kenqzinro.wiki.fc2.com/" target="_blank" rel="noopener">研究者の人狼研究（By研究者さん）</a><br>
          ・<a href="https://hanyayuraku.wiki.fc2.com/wiki/" target="_blank" rel="noopener">wolf_games_wiki（By藤井しきさん）</a><br>
          ・<a href="https://zinrou.game-info.wiki/" target="_blank" rel="noopener">人狼onlinePL名鑑s</a><br>
          ・<a href="https://jamojinro.wiki.fc2.com/" target="_blank" rel="noopener">人狼攻略wiki（ByJamを持参さん）</a><span class="lw-archived" style="display:inline-block;margin-left:6px;padding:1px 6px;border-radius:4px;background:#64748b;color:#fff;font-size:11px;font-weight:700;vertical-align:middle;white-space:nowrap" title="現在は閲覧できません。記録として残しています。">閉鎖</span><br>
          ・<a href="https://w.atwiki.jp/sirokumairoiro/" target="_blank" rel="noopener">新人狼OnlineWiki（Byしろくまさん）</a><br>
          ・<a href="https://vemi.pages.dev/" target="_blank" rel="noopener">氷のブログ</a><br>
          ・<a href="https://milkybot.wiki.fc2.com/wiki/" target="_blank" rel="noopener">Milkyの裏人狼onlinewiki（ByMilkyさん）</a><br>
          ・<a href="https://w.atwiki.jp/zensinkurotaitu/" target="_blank" rel="noopener">基本配役村ー人狼Online inー（By全身黒タイツさん）</a><br>
          ・<a href="https://w.atwiki.jp/kanjosen/" target="_blank" rel="noopener">環状線の人狼Onlinewiki（By環状線/てこさん）</a><br>
          ・<a href="https://w.atwiki.jp/casin_wiki/" target="_blank" rel="noopener">猫猫村/casin_wiki（ByCasinさん）</a><br>
          ・<a href="https://yuppu.wiki.fc2.com/" target="_blank" rel="noopener">人狼Online･トリップの会（Byゆっぷさん）</a><br>
        </p>

        <div style="height:6px"></div>

        <div style="height:6px"></div>
      </div>

      <div class="card">
        <h2>おねがい</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          人狼オンライン関連のリンクを募集中です。リンクは随時募集中です。<a href="https://x.com/mitdok/" target="_blank" rel="noopener">@mitdok</a>までお寄せください。
        </p>
      </div>
    </div>
  </section>


  <!-- Chrome拡張 -->
  <section id="tab-chrome" class="tab-panel" style="display:none">
    <div class="layout">
      <div class="card">
        <h2>機能概要</h2>
        <p style="font-size:.8rem;color:var(--muted);">
人狼オンライン（zinro.net）の画面に、戦績確認やRM管理を補助するボタンとパネルを追加します。<br>
最新版は <b>v1.6.20</b>（Chromeウェブストア申請準備中／現在のストア版はv1.6.10）です。<br>
<br>
<b>戦績・プレイヤー情報</b><br>
・村にいる全プレイヤーの戦績を一覧表示し、個別戦績・初登場年月・最近のゲームログを確認できます<br>
・戦績をチャット向けの一行形式でコピーできます<br>
・「今日の顔ぶれ」で平均レート、初心者・ベテラン・割れ酉・酉違いの人数を確認できます<br>
・称号バッジ、普段と異なるトリップの警告、自分との同村・同陣営・敵対・投票記録（因縁）を表示します<br>
・ゲーム終了後に今日のMVPを表示し、チャットへ投稿できます<br>
<br>
<b>RM・村管理支援</b><br>
・代表的な4～19人配役をワンクリックで設定し、開始時に配役人数の不一致を警告します<br>
・RMの「点呼」発言後に参戦者本人の発言を監視し、参戦者一覧へ「● 在席」「○ 未確認」を表示します（RM自身と観戦者は対象外）<br>
・手動KICK、トリップ／名前ブラックリスト、再入室時の自動追放に対応します<br>
・割れ酉リストによる警告・自動追放に対応し、RM実績10戦以上のトリップは既定で免除します<br>
・自動追放はRMがまとめてON／OFFでき、個別に割れ酉追放やRM実績免除も設定できます<br>
・公開トリップの最多利用名と参戦数を戦績DBで照合し、RMが指定した参戦数以上で名前が一致しない入村者を任意で自動追放できます（初期設定OFF）<br>
・入村前の村一覧でもRM名とトリップを照合し、村説明欄へ本物・偽物・判定不可を表示します<br>
・繰り返し発言や短時間の類似連投を検知し、参戦者を観戦へ移動できます<br>
・RMとの同村回数・過去ログ、村一覧のRM戦績バッジと並べ替え、入村中の他村一覧を表示します<br>
<br>
<b>そのほか</b><br>
・フレンド登録と他村でのオンライン確認、掲示板、利用時間・入村履歴の表示に対応します<br>
・設定やブラックリストはChrome内に保存され、再読み込み後も引き継がれます<br>
・拡張を更新した直後に限り、主な更新内容を1回だけポップアップでお知らせします<br>
</p>
<h2>インストール要件</h2>
<p style="font-size:.8rem;color:var(--muted);">
Mac/Windows/Chromebook/Ubuntu他Linuxなどの、Chrome系ブラウザが対象です。<br>スマホ（iOS/Android）は対象外です。</p>

<h2>インストール方法</h2>
<p style="font-size:.8rem;color:var(--muted);">
<a href=https://chromewebstore.google.com/detail/igcfdloddnajaapijiokljkichfmoocd?utm_source=item-share-cb>chromeウェブストア</a>にアクセスしてChromeに追加をクリック。実績の少ないアプリなので「セーフブラウジング保護強化機能で信頼されていません」という警告が出ますが気にせずインストールに進むをクリックしてください。拡張機能を追加をクリックでインストール完了です。<br>
</p>

<h2>年齢や管理端末の場合による制限</h2>
<p style="font-size:.8rem;color:var(--muted);">
本Chrome拡張（戦績解析ツール）は、Google Chromeの拡張機能です。Googleアカウントの年齢設定や管理状態によって、インストールが制限される場合があります。<br>
1. 年齢による制限について<br>
・13歳未満のアカウント → Googleのポリシー上、一般的なChrome拡張のインストールは制限されることがあります。（多くの場合、保護者管理アカウントとなります）<br>
2. 保護者管理（Family Link等）のアカウントの場合<br>
・拡張機能のインストールが 保護者の許可制 になっている<br>
・すべての拡張機能が一律でブロックされている<br>
といった設定が行われている場合があります。この場合、保護者の方がChromeの管理画面から拡張機能の利用を許可する必要があります。<br>
3. 学校・組織が管理している端末の場合<br>
・管理者が拡張機能のインストール自体を禁止している<br>
・許可された拡張機能以外は利用できない<br>
といった制限がかかっていることがあります。この制限は年齢に関係なく適用され、個人の設定では解除できません。<br>
</p>
    </div>

      <div class="card">

<h2>使用例（Youtube動画）</h2>
<p style="font-size:.8rem;color:var(--muted);">
<div style="position:relative; width:100%; aspect-ratio:9 / 16;">
  <iframe
    src="https://www.youtube.com/embed/Vb1WODT2d4M"
    title="人狼オンライン戦績解析機（サプリの小道具）Chrome拡張機能紹介"
    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
</p>
    </div>

      <div class="card release-history" style="grid-column:1 / -1;">
        <h2>更新履歴</h2>
        <p style="font-size:.78rem;color:var(--muted);">Manifest、配布ZIP、Git差分と実装コードを確認して整理した履歴です。項目を開くと詳細を表示します。</p>

        <details open>
          <summary>v1.6.20（2026-09-21）— 更新直後の変更内容ポップアップ</summary>
          <ul>
            <li>拡張を更新した直後、最初の人狼オンライン画面で主な変更内容を1回だけ表示。</li>
            <li>今回の案内には、村一覧でのRM本人判定と、しきい値による名前不一致者の自動追放を記載。</li>
            <li>新規インストール・通常起動・ページ再読み込みでは再表示しない。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.19（2026-09-21）— 入村前のRM本人判定</summary>
          <ul>
            <li>村一覧に表示されたRM名・公開トリップを、戦績DBの最多利用名と照合。</li>
            <li>村説明欄へ「本物」「偽物」「判定不可」と、最多利用名・総参戦数を表示。</li>
            <li>RM管理で保存した最低参戦数を、入村前の判定にも共通利用。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.18（2026-09-21）— トリップ本人名の照合・自動追放</summary>
          <ul>
            <li>公開トリップごとに、戦績DB上の最多利用名と総参戦数を一括照会。</li>
            <li>RMが設定した最低参戦数以上で、現在の入村名が最多利用名と異なる場合だけ自動追放。</li>
            <li>初期設定はOFF。戦績なし・非公開トリップ・API障害時は追放せず、対象トリップをブラックリストにも登録しない安全設計。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.17（2026-09-21）— 点呼後の在席確認を改善</summary>
          <ul>
            <li>RMがチャットで「点呼」「点呼します」と発言した場合にも自動的に監視を開始。</li>
            <li>点呼発言より後の参戦者本人の発言だけを在席確認として扱い、開始直後の返答が漏れる問題を修正。</li>
            <li>参戦者一覧と点呼パネルに「● 在席」「○ 未確認」を表示し、全員確認後もゲーム開始まで表示を維持。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.16（2026-09-07）— 掲示板のトリップ入力を修正</summary>
          <ul>
            <li>キーのみ、#キー、名前#キーのどの形式でも同じトリップに変換。</li>
            <li>##セキュアキーと名前##セキュアキーに対応し、通常の#キーを誤って拒否していた問題を修正。</li>
            <li>トリップキー欄をパスワード表示に変更。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.15（2026-08-26）— RM実績による割れ酉追放免除</summary>
          <ul>
            <li>村を10回以上建てた実績のあるトリップを、割れ酉自動追放から既定で免除。</li>
            <li>免除設定はRM管理画面でON/OFFでき、ブラウザ内に保存。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.14（2026-08-26）— 自動追放の選択権を追加</summary>
          <ul>
            <li>トリップBL・名前BL・割れ酉の自動追放をまとめて停止できるマスタースイッチを追加。</li>
            <li>割れ酉の自動追放は、RMが明示的にONにした設定に従うよう修正。手動KICKは常時利用可能。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.13（2026-08-26）— 顔ぶれ・称号・因縁・MVP</summary>
          <ul>
            <li>「今日の顔ぶれ」、平均レート、初心者・ベテラン・割れ酉・酉違いの集計を追加。</li>
            <li>称号バッジ、酉違い警告、自分との同村・同陣営・敵対・相互投票記録を表示する「因縁」を追加。</li>
            <li>ゲーム終了時の今日のMVP表示と、チャット投稿ボタンを追加。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.12（2026-08-21）— 割れ酉表示の説明を追加</summary>
          <ul>
            <li>戦績一覧に割れ酉が含まれる場合だけ、末尾に「⚠️」の意味と注意事項を表示。</li>
            <li>キャッシュから戦績を復元した場合にも同じ注記を表示。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.11（2026-07-27）— Gemini連携削除・権限縮小</summary>
          <ul>
            <li>Gemini APIキー保存、ログ送信、直接解析、解析結果UIを削除。</li>
            <li>接続権限を zinro.net と mitdok.dokasen.com のみに縮小。</li>
            <li>通信インターセプトのURL判定を改善し、RM同村履歴など既存のRM支援は維持。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.10（2026-06-29）— 点呼・通信・スパム対策の安定化</summary>
          <ul>
            <li>zinro.netへの通信をキュー化し、APIや管理操作の連続アクセスを抑制。</li>
            <li>点呼状態をページ再読み込み後も復元し、人数判定、未確認者表示、落ち中断、チャットDOM判定を改善。</li>
            <li>危険な結合文字、同一フレーズの繰り返し、短時間の類似連投を検知する自動追放と処理キューを追加。</li>
          </ul>
        </details>
        <details>
          <summary>v1.6.9（2026-06-20／点呼修正版 06-29）— 基礎機能</summary>
          <ul>
            <li>戦績一覧・個別戦績・最近のログ・コピー、配役プリセット、入村履歴、利用時間、フレンド、掲示板を実装。</li>
            <li>RM管理、手動／自動KICK、トリップ・名前BL、割れ酉対応、点呼、落ち警告、観戦移動を実装。</li>
            <li>村一覧のRM戦績、他村表示、RM過去ゲーム・同村履歴を実装。当初含まれたGemini解析はv1.6.11で削除。</li>
            <li>同一バージョン内修正版で、RM除外、開始後の任意発言による点呼確認、過去発言の重複防止を追加。</li>
          </ul>
        </details>
      </div>
    </div>
  </section>

    <!-- コマンド -->
  <!-- RM集計 -->
  <!-- 変名図（トリップ⇄名前の連鎖系譜） -->
  <section id="tab-chains" class="tab-panel" style="display:none">
    <div class="layout">
      <div class="card" id="chainsSearchCard" style="grid-column: 1 / -1;">
        <h2>系譜検索</h2>
        <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
          <label style="font-size:.75rem;color:var(--muted);">トリップ</label>
          <input id="chainsTripQuery" type="text" placeholder="例: ◆SuppleLohk" style="width:200px;">
          <button id="chainsTripSearchBtn" class="btn btn-primary" type="button">系譜を開く</button>
          <span style="width:16px;"></span>
          <label style="font-size:.75rem;color:var(--muted);">名前</label>
          <input id="chainsNameQuery" type="text" placeholder="例: サプリ" style="width:200px;">
          <button id="chainsNameSearchBtn" class="btn btn-primary" type="button">トリップを探す</button>
        </div>
        <div id="chainsNameResults" style="margin-top:8px;"></div>
      </div>

      <div class="card" id="chainsListCard">
        <h2>変名図（トリップの系譜インデックス）</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          10戦以上あるトリップを、戦数が多い順に並べたインデックスです。<br>
          「系譜を見る」で、そのトリップを起点に変名の系譜を最古ログまで振り返れます。
        </p>
        <div id="chainsLoading" style="display:none;margin:6px 0 8px;padding:6px 10px;border-radius:999px;border:1px solid var(--accent);background:var(--accent-soft);color:var(--accent);font-size:.8rem;font-weight:600;">
          読み込み中です…
        </div>
        <div class="table-wrap">
          <table id="chainsTable">
            <thead>
              <tr>
                <th>#</th>
                <th>主な名前</th>
                <th>トリップ</th>
                <th class="num">戦数</th>
                <th>最終参戦</th>
                <th>初参戦</th>
                <th></th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div id="chainsPager" style="margin-top:4px;font-size:.75rem;color:var(--muted);"></div>
      </div>
      <div class="card" id="chainsHelpCard">
        <h2>説明</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          ・系譜は「起点トリップが名乗った名前 → その名前が使った別トリップ → …」の関連を、ゲーム最古ログまで連鎖的に辿った結果です。<br>
          ・一瞬でも同じ名前を名乗った実績があれば関連ありとみなします。そのため「田中」など汎用名を経由すると別人が混ざり得ます（棒の色が濃いほど起点から遠い関連です）。<br>
          ・規模が大きすぎる系譜は上位から一部のみ表示し、その旨を注記します。<br>
          ・トリップをクリックすると個人戦績へ飛べます。データは毎朝4時更新。<br>
        </p>
      </div>

      <div class="card" id="lineageFigCard" style="display:none; grid-column: 1 / -1;">
        <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
          <button id="lineageBackBtn" class="btn btn-ghost" type="button">← 一覧へ戻る</button>
          <h2 id="lineageFigTitle" style="margin:0;">系譜図</h2>
        </div>
        <div id="lineageShareRow" style="display:flex;align-items:center;gap:6px;margin:8px 0;flex-wrap:wrap;">
          <span style="font-size:.75rem;color:var(--muted);">共有リンク:</span>
          <input id="lineageShareUrl" type="text" readonly style="flex:1 1 320px;font-size:.75rem;">
          <button id="lineageShareCopyBtn" class="btn btn-ghost" type="button" style="font-size:.75rem;">コピー</button>
        </div>
        <div id="lineageFigBody" style="overflow-x:auto;"></div>
      </div>
    </div>
  </section>

  <section id="tab-rm-stats" class="tab-panel" style="display:none">
    <h2>RM集計</h2>
    <p style="margin-bottom:12px;opacity:.7;font-size:13px;">RM（村建て人）ごとのゲーム数・使用村名・配役などを集計します。</p>
    <div style="display:flex;gap:8px;margin-bottom:16px;align-items:center;">
      <input id="rm-stats-search" type="text" placeholder="RM名で検索" style="flex:1;padding:8px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.15);background:rgba(0,0,0,.3);color:#fff;font-size:13px;">
      <button id="rm-stats-search-btn" style="padding:8px 16px;border-radius:6px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-size:13px;cursor:pointer;">検索</button>
    </div>
    <div id="rm-stats-content">
      <div style="opacity:.5;">読み込み中...</div>
    </div>
  </section>

  <section id="tab-bbs" class="tab-panel" style="display:none">
    <div class="layout">
      <div class="card">
        <h2>掲示板</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          サイトへの連絡、要望、雑談などに使えます。トリップキーは任意です。
        </p>
        <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;">
          <div style="flex:1 1 160px;">
            <label>名前</label>
            <input id="bbsNameInput" type="text" maxlength="30" placeholder="名前">
          </div>
          <div style="flex:1 1 180px;">
            <label>トリップキー（任意）</label>
            <input id="bbsTripKeyInput" type="text" maxlength="100" placeholder="#key / ##key" autocomplete="off">
          </div>
        </div>
        <div style="height:6px"></div>
        <textarea id="bbsBodyInput" maxlength="100" placeholder="本文（100文字まで）"></textarea>
        <div style="height:6px"></div>
        <button id="bbsPostBtn" class="btn btn-primary">投稿</button>
        <span id="bbsPostMsg" style="margin-left:8px;font-size:.8rem;color:var(--muted);"></span>
      </div>
      <div class="card">
        <h2>投稿一覧</h2>
        <div id="bbsList" style="font-size:.85rem;color:var(--muted);">読み込み中…</div>
        <div id="bbsPager" style="margin-top:8px;font-size:.75rem;color:var(--muted);"></div>
      </div>
    </div>
  </section>

  <section id="tab-command" class="tab-panel" style="display:none">
    <div class="layout">
      <div class="card">
        <h2>入村前</h2>
        <p style="font-size:.8rem;color:var(--muted);">
・入村コマンド<br>
https://zinro.net/m/room_in.php?mode=room_in&room=村名&name=自分の名前<br>
・トリップ非公開<br>
https://zinro.net/m/room_in.php?mode=room_in&room=村名&name=自分の名前&trip=null<br>
・村を作るコマンド<br>
https://zinro.net/m/room_create.php?mode=room_create&room=村名&name=自分の名前&room_comment=村説明&room_luar=掟&limit=定員の人数&password=合言葉<br>
・無敵村作成<br>
https://zinro.net/m/room_create.php?mode=room_create&room=村名&name=自分の名前&room_comment=村説明&room_luar=掟&limit=定員の人数&passworld=合言葉&MUTEKI<br>
・GMモード<br>
https://zinro.net/m/room_create.php?mode=room_create&room=村名&name=自分の名前&room_comment=村説明&room_luar=掟&limit=定員の人数&passworld=合言葉&GM<br>
・村リストコマンド<br>
開始前<br>
https://zinro.net/m/room_list.php?mode=before<br>
終了<br>
https://zinro.net/m/room_list.php?mode=finish<br>
ゲーム中<br>
https://zinro.net/m/room_list.php?mode=playing<br>
        </p>

        <h2>入村後</h2>
        <p style="font-size:.8rem;color:var(--muted);">
・囁き<br>
https://zinro.net/m/player.php?mode=massage&from_user=自分の名前&to_user=相手の名前&massage=発言内容<br>
・RM乗っ取り<br>
https://zinro.net/m/player.php?mode=su<br>
・掟同意<br>
https://zinro.net/m/player.php?mode=commit_room_rule<br>
・退室<br>
https://zinro.net/m/end.php?ad=inter<br>
・発言フォーム/行数切り替え<br>
https://zinro.net/m/player.php?mode=change_input&line=行数<br>
・観戦者コメント<br>
表示<br>
https://zinro.net/m/player.php?audience=1<br>
非表示<br>
https://zinro.net/m/player.php?audience=0<br>
・村情報json取得<br>
https://zinro.net/m/json/?mode=get&room=村名<br>
・軽量版切替<br>
軽量版オン<br>
https://zinro.net/m/player.php?mode=light<br>
軽量版オフ<br>
https://zinro.net/m/player.php?mode=heavy<br>
        </p>

        <h2>ゲーム中</h2>
        <p style="font-size:.8rem;color:var(--muted);">
・投票コマンド<br>
https://zinro.net/m/player.php?vote=投票相手の名前<br>
・役職実行（狼/占/狩）<br>
https://zinro.net/m/player.php?mode=joajdon&to_user=実行する相手の名前<br>
        </p>

        <h2>RM用</h2>
        <p style="font-size:.8rem;color:var(--muted);">
・終了<br>
https://zinro.net/m/end.php?massage=<br>
・役職設定<br>
https://zinro.net/m/player.php?mode=updateJobset&job_1=3&job_3=1&job_4=1&job_5=1&job_6=1&job_10=1&yakukakenashi=0<br>
・お題役設定<br>
https://zinro.net/m/player.php?mode=set_jobset&world_gm=名前<br>
・ワンナイト切替<br>
https://zinro.net/m/player.php?game_mode=one_night<br>
・ワードウルフ切替<br>
https://zinro.net/m/player.php?game_mode=world_wolf<br>
・開始<br>
https://zinro.net/m/player.php?game=start<br>
・中断<br>
https://zinro.net/m/player.php?game=stop<br>
・合言葉変更<br>
https://zinro.net/m/player.php?set_passworld&passworld=合言葉<br>
・説明文変更<br>
https://zinro.net/m/player.php?set_room_comment&room_comment=村説明
・掟変更
https://zinro.net/m/player.php?set_room_luar&room_luar=村説明<br>
・シーン変更<br>
https://zinro.net/m/player.php?mode=change_scene<br>
・延長コマンド<br>
https://zinro.net/m/player.php?mode=extend<br>
・CPU追加<br>
https://zinro.net/m/player.php?addCpu=1c<br>
・定員設定<br>
https://zinro.net/m/player.php?mode=set_limit&limit=人数<br>
・お題設定<br>
https://zinro.net/m/player.php?mode=set_theme&theme=お題<br>
        </p>

        <div style="height:6px"></div>

        <div style="height:6px"></div>
      </div>

      <div class="card">
        <h2>おねがい</h2>
        <p style="font-size:.8rem;color:var(--muted);">
          対策されたり追加されたなど情報お願いします。<a href="https://x.com/mitdok/" target="_blank" rel="noopener">@mitdok</a>までお寄せください。このコマンド列挙にあたって<a href="https://milkybot.wiki.fc2.com/wiki/" target="_blank" rel="noopener">Milkyさんのwiki</a>を参考（パクリ）にしました。多大なる感謝です。Milkyさんの区分にサプリが感じた違和感のものを修正したり、サプリが知ってるコマンドは正しいのに修正しましたが、全部は試してないです。動かないの結構あるはず。API表示コマンドとかあったはずだけど、手元にスグないのでいまやれてない。。
        </p>
      </div>
    </div>
  </section>


</main>
</div><!-- /wrapper -->


<!-- ══════════════════════════════════════════════════════════════════════════
     ログイン / Wiki / コメント / プロフィール
     wiki_api.php (PHP直結) に対して fetch する
══════════════════════════════════════════════════════════════════════════ -->
<script>
(function () {
  'use strict';

  /* ── 設定 ──────────────────────────────────────────────────────────────── */
  const WIKI_API  = 'wiki_api.php';
  const STORE_KEY = 'zinro_jwt';
  const LOGIN_IDENTITY_KEY = 'zinro_login_identity';

  /* ── State ─────────────────────────────────────────────────────────────── */
  let auth = { loggedIn: false, trip: '', name: '' };
  let ctx  = { key_type: '', key_value: '' };

  // initAuth 完了を待てる Promise
  let _authResolve;
  const authReady = new Promise(res => { _authResolve = res; });

  /* ══════════════════════════════════════════════════════════════════════════
     fetch インターセプター
     app.js が /api.php?mode=xxx を呼ぶとき:
       WIKI_MODES → wiki_api.php にリダイレクト＋レスポンス正規化
       それ以外 (personal, monthly_* 等) → Docker にそのまま通す
  ══════════════════════════════════════════════════════════════════════════ */
  (function installFetchInterceptor() {
    const WIKI_MODES = new Set([
      'login','logout','me',
      'wiki_get','wiki_save',
      'comment_list','comment_add',
      'profile_get','profile_update',
    ]);
    const _orig = window.fetch.bind(window);

    window.fetch = function (input, init) {
      try {
        const urlStr = (input instanceof Request) ? input.url : String(input);
        const u = new URL(urlStr, location.href);
        if (/\/api\.php(\?|$)/.test(u.pathname + u.search) || /\/api\.php$/.test(u.pathname)) {
          const mode = u.searchParams.get('mode')
            || (() => { try { return JSON.parse((init||{}).body||'{}').mode; } catch { return ''; } })();
          if (mode && WIKI_MODES.has(mode)) {
            const newUrl = u.href.replace(/\/api\.php\b/, '/wiki_api.php');
            const newInit = init ? { ...init } : {};
            return _orig(newUrl, newInit).then(async r => {
              const raw = await r.text();
              let parsed = {};
              try { parsed = JSON.parse(raw); } catch {}
              if (r.ok) {
                const merged = { ok: true, status: 'ok', result: 'success', success: true, ...parsed };
                return new Response(JSON.stringify(merged), {
                  status: 200, headers: { 'Content-Type': 'application/json' },
                });
              }
              return new Response(raw, {
                status: r.status, headers: { 'Content-Type': 'application/json' },
              });
            }).catch(() => _orig(newUrl, init));
          }
        }
      } catch {}
      return _orig(input, init);
    };
  })();

  /* ── 汎用ヘルパー ──────────────────────────────────────────────────────── */
  const $  = id => document.getElementById(id);
  const esc = s  => String(s)
    .replace(/&/g,'&amp;').replace(/</g,'&lt;')
    .replace(/>/g,'&gt;').replace(/"/g,'&quot;');

  function nl2br(text) {
    return esc(text)
      .replace(/\n/g, '<br>')
      .replace(/(https?:\/\/[^\s<&"]+)/g,
        url => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
  }

  function getToken()  { return localStorage.getItem(STORE_KEY); }
  function setToken(t) { t ? localStorage.setItem(STORE_KEY, t) : localStorage.removeItem(STORE_KEY); }

  function restoreLoginIdentity() {
    try {
      const saved = JSON.parse(localStorage.getItem(LOGIN_IDENTITY_KEY) || 'null');
      if (!saved || typeof saved !== 'object') return;
      if ($('loginNameInput')) $('loginNameInput').value = String(saved.name || '');
      if ($('loginTripKeyInput')) $('loginTripKeyInput').value = String(saved.tripKey || '');
      if ($('loginRememberInput')) $('loginRememberInput').checked = true;
    } catch {
      localStorage.removeItem(LOGIN_IDENTITY_KEY);
    }
  }

  function saveLoginIdentity(name, tripKey) {
    if ($('loginRememberInput')?.checked) {
      localStorage.setItem(LOGIN_IDENTITY_KEY, JSON.stringify({ name, tripKey }));
    } else {
      localStorage.removeItem(LOGIN_IDENTITY_KEY);
    }
  }

  function authHdr() {
    const h = { 'Content-Type': 'application/json' };
    const t = getToken();
    if (t) h['Authorization'] = 'Bearer ' + t;
    return h;
  }

  async function apiGET(mode, params = {}) {
    const qs = new URLSearchParams({ mode, ...params }).toString();
    return fetch(WIKI_API + '?' + qs, { headers: authHdr() });
  }
  async function apiPOST(mode, body = {}) {
    return fetch(WIKI_API + '?mode=' + mode, {
      method: 'POST', headers: authHdr(), body: JSON.stringify(body),
    });
  }

  /* ══════════════════════════════════════════════════════════════════════════
     認証
  ══════════════════════════════════════════════════════════════════════════ */

  async function initAuth() {
    if (!getToken()) { renderLoggedOut(); _authResolve(); return; }
    try {
      const r = await apiGET('me');
      if (!r.ok) { setToken(null); renderLoggedOut(); _authResolve(); return; }
      const d = await r.json();
      auth = { loggedIn: true, trip: d.trip, name: d.name };
      renderLoggedIn();
      loadProfileIntoForm();
    } catch { renderLoggedOut(); }
    _authResolve();
  }

  /* ── ヘッダー + プロフィールタブの表示切替 ── */
  function renderLoggedIn() {
    const { trip, name } = auth;
    const disp = (name ? esc(name) + '　' : '') + esc(trip);

    // ヘッダー：ログインエリアを隠してユーザーエリアを表示
    const hla = $('headerLoginArea');  if (hla) hla.style.display = 'none';
    const hua = $('headerUserArea');
    if (hua) {
      hua.style.display = 'flex';
      const un = $('headerUserName');
      if (un) un.innerHTML = `ログイン中: <b>${disp}</b>`;
    }
    closeHeaderLoginForm();

    // プロフィールタブ切替
    const pnl = $('profileNotLoggedIn'); if (pnl) pnl.style.display = 'none';
    const pll = $('profileLoggedIn');
    if (pll) {
      pll.style.display = 'block';
      const lu = $('profileLoggedInUser');
      if (lu) lu.innerHTML = `ログイン中: <b>${disp}</b>`;
    }

    refreshPersonalAuthUI();
  }

  function renderLoggedOut() {
    auth = { loggedIn: false, trip: '', name: '' };

    // ヘッダー
    const hla = $('headerLoginArea'); if (hla) hla.style.display = 'block';
    const hua = $('headerUserArea');  if (hua) hua.style.display = 'none';
    closeHeaderLoginForm();

    // プロフィールタブ
    const pnl = $('profileNotLoggedIn'); if (pnl) pnl.style.display = 'block';
    const pll = $('profileLoggedIn');    if (pll) pll.style.display = 'none';

    refreshPersonalAuthUI();
  }

  /* ── ヘッダーログインフォーム開閉 ── */
  function openHeaderLoginForm() {
    const f = $('headerLoginForm'); if (f) f.style.display = 'block';
    // フォームを開いたら外クリックで閉じる
    setTimeout(() => {
      document.addEventListener('click', outsideLoginClose, { capture: true, once: true });
    }, 0);
  }
  function closeHeaderLoginForm() {
    const f = $('headerLoginForm'); if (f) f.style.display = 'none';
    showLoginMsg('');
  }
  function outsideLoginClose(e) {
    const form = $('headerLoginForm');
    const btn  = $('headerLoginToggleBtn');
    if (form && !form.contains(e.target) && e.target !== btn) {
      closeHeaderLoginForm();
    } else {
      // フォーム内クリックはもう1回登録し直す
      document.addEventListener('click', outsideLoginClose, { capture: true, once: true });
    }
  }

  /* ── ログインハンドラー ── */
  async function handleLogin() {
    const tripKey = ($('loginTripKeyInput')?.value ?? '').trim();
    const name    = ($('loginNameInput')?.value    ?? '').trim();
    if (!name) { showLoginMsg('名前を入力してください', true); $('loginNameInput')?.focus(); return; }
    if (!tripKey) { showLoginMsg('トリップキーを入力してください', true); return; }

    const btn = $('loginBtn');
    if (btn) { btn.disabled = true; btn.textContent = 'ログイン中…'; }
    showLoginMsg('');

    try {
      const r = await apiPOST('login', { trip_key: tripKey, name });
      let d = {};
      try { d = await r.json(); } catch {}
      if (!r.ok) { showLoginMsg(d.error || 'ログイン失敗', true); return; }
      saveLoginIdentity(name, tripKey);
      setToken(d.token);
      auth = { loggedIn: true, trip: d.trip, name: d.name };
      renderLoggedIn();
      loadProfileIntoForm();
      closeHeaderLoginForm();
      if (ctx.key_type && ctx.key_value) loadWikiCommentProfile();
    } catch (e) {
      showLoginMsg('通信エラー: ' + e.message, true);
    } finally {
      if (btn) { btn.disabled = false; btn.textContent = 'ログイン'; }
    }
  }

  function showLoginMsg(msg, isErr = false) {
    const el = $('loginMsg');
    if (!el) return;
    el.style.color = isErr ? 'var(--danger,#f87171)' : 'var(--accent)';
    el.textContent = msg;
  }

  function handleLogout() {
    setToken(null);
    renderLoggedOut();
  }

  /* ══════════════════════════════════════════════════════════════════════════
     個人戦績タブの認証UI更新
  ══════════════════════════════════════════════════════════════════════════ */

  function refreshPersonalAuthUI() {
    const info    = $('personalLoginInfo');
    const wikiArea = $('wikiEditToggleArea');
    const cmtForm  = $('commentForm');
    const hint     = $('profileLoginHint');
    const cname    = $('comment-author-name');

    if (auth.loggedIn) {
      if (info)    info.textContent = `ログイン中: ${auth.name ? auth.name + '　' : ''}${auth.trip}`;
      if (wikiArea) wikiArea.style.display = 'block';
      if (cmtForm)  cmtForm.style.display  = 'block';
      if (hint)     hint.style.display     = 'none';
      if (cname)    cname.textContent      = auth.name || auth.trip;
    } else {
      if (info)    info.textContent = 'ログインしていません。';
      if (wikiArea) wikiArea.style.display = 'none';
      if (cmtForm)  cmtForm.style.display  = 'none';
      if (hint)     hint.style.display     = 'block';
    }
  }

  /* ══════════════════════════════════════════════════════════════════════════
     Wiki / コメント / プロフィール 読み込み
  ══════════════════════════════════════════════════════════════════════════ */

  async function loadWikiCommentProfile() {
    await authReady;
    refreshPersonalAuthUI();
    if (ctx.key_type === 'trip') {
      loadProfile(ctx.key_value);
      const ps = $('profileSection');
      if (ps) ps.style.display = 'block';
    } else {
      const ps = $('profileSection');
      if (ps) ps.style.display = 'none';
    }
    loadWiki();
    loadComments();
  }

  /* ── Wiki ── */
  async function loadWiki() {
    const el = $('wikiViewBody');
    if (!el) return;
    try {
      const r = await apiGET('wiki_get', { key_type: ctx.key_type, key_value: ctx.key_value });
      if (!r.ok) return;
      const d = await r.json();
      el.innerHTML = d.body ? nl2br(d.body) : 'まだWikiは登録されていません。';
    } catch { el.textContent = 'まだWikiは登録されていません。'; }
  }

  function toggleWikiEdit() {
    const view = $('wikiView');
    const edit = $('wikiEdit');
    const saveBtn   = $('wikiSaveBtn');
    const cancelBtn = $('wikiEditCancelBtn');
    if (!view || !edit) return;
    const isEditing = edit.style.display !== 'none';
    view.style.display = isEditing ? 'block' : 'none';
    edit.style.display = isEditing ? 'none'  : 'block';
    if (saveBtn)   saveBtn.style.display   = isEditing ? 'none' : 'inline-block';
    if (cancelBtn) cancelBtn.style.display = isEditing ? 'none' : 'inline-block';
    if (!isEditing) {
      const body = $('wikiBody');
      if (body) body.value = $('wikiViewBody')?.textContent?.replace(/<br>/g, '\n') ?? '';
    }
  }

  function cancelWikiEdit() {
    const view = $('wikiView');
    const edit = $('wikiEdit');
    const saveBtn   = $('wikiSaveBtn');
    const cancelBtn = $('wikiEditCancelBtn');
    if (view) view.style.display = 'block';
    if (edit) edit.style.display = 'none';
    if (saveBtn)   saveBtn.style.display   = 'none';
    if (cancelBtn) cancelBtn.style.display = 'none';
  }

  async function saveWiki(e) {
    if (e) { e.stopPropagation(); e.stopImmediatePropagation(); }
    if (!auth.loggedIn) return;
    const body = ($('wikiBody')?.value ?? '').trim();
    const msg  = $('wikiSaveMsg');
    const btn  = $('wikiSaveBtn');
    if (btn) btn.disabled = true;
    if (msg) msg.textContent = '保存中…';
    try {
      const r = await apiPOST('wiki_save', {
        key_type: ctx.key_type, key_value: ctx.key_value, body,
      });
      let d = {};
      try { d = await r.json(); } catch {}
      if (!r.ok) { if (msg) msg.textContent = '⚠ ' + (d.error || '保存失敗'); return; }
      if (msg) msg.textContent = '✅ 保存しました';
      setTimeout(() => { if (msg) msg.textContent = ''; }, 3000);
      cancelWikiEdit();
      await loadWiki();
    } catch (e2) {
      if (msg) msg.textContent = '⚠ 通信エラー: ' + e2.message;
    } finally {
      if (btn) btn.disabled = false;
    }
  }

  /* ── コメント ── */
  async function loadComments() {
    const el = $('commentList');
    if (!el) return;
    try {
      const r = await apiGET('comment_list', { key_type: ctx.key_type, key_value: ctx.key_value });
      if (!r.ok) return;
      const d = await r.json();
      const list = d.comments || d.data || [];
      if (!list.length) { el.textContent = 'コメントはまだありません。'; return; }
      el.innerHTML = list.map(c => {
        const dt = c.created_at ? new Date(c.created_at * 1000).toLocaleString('ja-JP') : '';
        return `<div style="border-bottom:1px solid var(--border);padding:4px 0;">
          <span style="font-weight:600;">${esc(c.author_name || '名無しさん')}</span>
          <span style="color:var(--muted);font-size:.75rem;margin-left:6px;">${dt}</span><br>
          <span>${nl2br(c.body || '')}</span>
        </div>`;
      }).join('');
    } catch { el.textContent = 'コメントの読み込みに失敗しました。'; }
  }

  async function sendComment(e) {
    if (e) { e.stopPropagation(); e.stopImmediatePropagation(); }
    if (!auth.loggedIn) return;

    const msg = $('commentSendMsg');
    if (!ctx.key_type || !ctx.key_value) {
      if (msg) msg.textContent = '⚠ 対象が選択されていません';
      return;
    }
    const ta   = $('commentBody');
    const text = (ta?.value ?? '').trim();
    if (!text) {
      if (msg) msg.textContent = '⚠ コメントを入力してください';
      return;
    }

    const btn = $('commentSendBtn');
    if (btn) btn.disabled = true;
    if (msg) msg.textContent = '送信中…';

    try {
      const r = await apiPOST('comment_add', {
        key_type: ctx.key_type, key_value: ctx.key_value, body: text,
      });
      let d = {};
      try { d = await r.json(); } catch {}
      if (!r.ok) { if (msg) msg.textContent = '⚠ ' + (d.error || '送信失敗'); return; }
      if (ta)  ta.value        = '';
      if (msg) msg.textContent = '✅ 送信しました';
      setTimeout(() => { if (msg) msg.textContent = ''; }, 3000);
      await loadComments();
    } catch (e2) {
      if (msg) msg.textContent = '⚠ 通信エラー: ' + e2.message;
    } finally {
      if (btn) btn.disabled = false;
    }
  }

  /* ══════════════════════════════════════════════════════════════════════════
     プロフィール（自己紹介）
  ══════════════════════════════════════════════════════════════════════════ */

  async function loadProfile(trip) {
    const el = $('profileView');
    if (!el) return;
    try {
      const r = await apiGET('profile_get', { trip });
      if (!r.ok) return;
      const d = await r.json();
      if (!d.self_intro && !d.x_id) {
        el.textContent = '自己紹介が登録されていません。';
        return;
      }
      let html = '';
      if (d.self_intro) html += '<div>' + nl2br(d.self_intro) + '</div>';
      if (d.x_id) {
        const xid  = String(d.x_id).replace(/^@/, '');
        const xdisp = esc(d.x_display || '@' + xid);
        html += `<div style="margin-top:4px;font-size:.8rem;">
          X: <a href="https://x.com/${esc(xid)}" target="_blank" rel="noopener noreferrer">${xdisp}</a>
        </div>`;
      }
      el.innerHTML = html;
    } catch { el.textContent = ''; }
  }

  async function loadProfileIntoForm() {
    if (!auth.loggedIn) return;
    try {
      const r = await apiGET('profile_get', { trip: auth.trip });
      if (!r.ok) return;
      const d = await r.json();
      const si = $('profileSelfIntro');
      const xi = $('profileXId');
      const xd = $('profileXDisplay');
      if (si) si.value = d.self_intro || '';
      if (xi) xi.value = d.x_id       || '';
      if (xd) xd.value = d.x_display  || '';
    } catch {}
  }

  async function saveProfile(e) {
    if (e) { e.stopPropagation(); e.stopImmediatePropagation(); }
    if (!auth.loggedIn) {
      const notice = $('profileEditNotice');
      if (notice) notice.textContent = '⚠ ログインが必要です';
      return;
    }
    const self_intro = ($('profileSelfIntro')?.value ?? '').trim();
    const x_id       = ($('profileXId')?.value       ?? '').trim();
    const x_display  = ($('profileXDisplay')?.value   ?? '').trim();

    const btn    = $('profileSaveBtn');
    const notice = $('profileEditNotice');
    if (btn) btn.disabled = true;

    try {
      const r = await apiPOST('profile_update', { self_intro, x_id, x_display });
      let d = {};
      try { d = await r.json(); } catch {}
      if (!r.ok) {
        if (notice) notice.textContent = '⚠ ' + (d.error || '保存失敗');
        return;
      }
      if (notice) {
        notice.textContent = '✅ 保存しました。個人戦績タブで確認できます。';
        setTimeout(() => {
          if (notice) notice.textContent = '保存すると、個人戦績タブで自分のトリップを検索した際に表示されます。';
        }, 4000);
      }
      if (ctx.key_type === 'trip' && ctx.key_value === auth.trip) {
        await loadProfile(auth.trip);
      }
    } catch (e2) {
      if (notice) notice.textContent = '⚠ 通信エラー: ' + e2.message;
    } finally {
      if (btn) btn.disabled = false;
    }
  }

  /* ══════════════════════════════════════════════════════════════════════════
     個人戦績タブ表示の検出
  ══════════════════════════════════════════════════════════════════════════ */

  function observePersonalResults() {
    const resultMain = $('personalResultMain');
    const ktEl       = $('key_type');
    const kvEl       = $('key_value');
    if (!resultMain) return;

    function onBecomeVisible() {
      const kt = ktEl?.value ?? '';
      const kv = kvEl?.value ?? '';
      if (kt && kv && (kt !== ctx.key_type || kv !== ctx.key_value)) {
        ctx = { key_type: kt, key_value: kv };
        loadWikiCommentProfile();
      } else if (kt && kv) {
        // 同じコンテキスト（タブ切り替え後等）
        authReady.then(() => refreshPersonalAuthUI());
      }
    }

    new MutationObserver(mutations => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style') {
          if (resultMain.style.display !== 'none') {
            setTimeout(onBecomeVisible, 80);
          }
        }
      }
    }).observe(resultMain, { attributes: true });

    if (resultMain.style.display !== 'none') {
      setTimeout(onBecomeVisible, 80);
    }
  }

  /* ══════════════════════════════════════════════════════════════════════════
     ログインタブへのリンク
  ══════════════════════════════════════════════════════════════════════════ */

  function setupGoLoginLinks() {
    document.addEventListener('click', e => {
      const target = e.target.closest('.go-login-tab-link');
      if (!target) return;
      e.preventDefault();
      const tabBtn = document.querySelector('[data-tab="tab-login"]');
      if (tabBtn) tabBtn.click();
    });
  }

  /* ══════════════════════════════════════════════════════════════════════════
     初期化
  ══════════════════════════════════════════════════════════════════════════ */

  document.addEventListener('DOMContentLoaded', () => {

    restoreLoginIdentity();

    // ── ボタンをクローン置換して app.js のリスナーを除去 ──
    function fresh(id) {
      const el = $(id);
      if (!el) return null;
      const c = el.cloneNode(true);
      el.parentNode.replaceChild(c, el);
      return c;
    }

    const loginBtn       = fresh('loginBtn');
    const logoutBtn      = fresh('logoutBtn');
    const wikiToggleBtn  = fresh('wikiEditToggleBtn');
    const wikiSaveBtn    = fresh('wikiSaveBtn');
    const wikiCancelBtn  = fresh('wikiEditCancelBtn');
    const commentSendBtn = fresh('commentSendBtn');
    const profileSaveBtn = fresh('profileSaveBtn');
    const hdrToggleBtn   = fresh('headerLoginToggleBtn');
    const hdrCancelBtn   = fresh('headerLoginCancelBtn');

    // ── 認証初期化 ──
    initAuth();

    // ── ヘッダーログイン ──
    hdrToggleBtn?.addEventListener('click', e => { e.stopPropagation(); openHeaderLoginForm(); });
    hdrCancelBtn?.addEventListener('click', e => { e.stopPropagation(); closeHeaderLoginForm(); });
    loginBtn    ?.addEventListener('click', handleLogin);
    logoutBtn   ?.addEventListener('click', handleLogout);
    $('loginTripKeyInput')?.addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); });
    $('loginNameInput')?.addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); });
    $('loginRememberInput')?.addEventListener('change', e => {
      if (!e.target.checked) localStorage.removeItem(LOGIN_IDENTITY_KEY);
    });

    // ── Wiki ──
    wikiToggleBtn?.addEventListener('click', e => { e.stopPropagation(); e.stopImmediatePropagation(); toggleWikiEdit(); }, true);
    wikiSaveBtn  ?.addEventListener('click', e => { e.stopPropagation(); e.stopImmediatePropagation(); saveWiki(e); }, true);
    wikiCancelBtn?.addEventListener('click', e => { e.stopPropagation(); e.stopImmediatePropagation(); cancelWikiEdit(); }, true);

    // ── コメント（stopImmediatePropagation で app.js 委任を遮断） ──
    commentSendBtn?.addEventListener('click', sendComment, true);

    // ── プロフィール保存 ──
    profileSaveBtn?.addEventListener('click', saveProfile, true);

    // ── 個人戦績表示の検出 ──
    observePersonalResults();

    // ── ログインタブへのリンク ──
    setupGoLoginLinks();
  });

})();
</script>

<!-- ══════════════════════════════════════════════════════════════════════════
     配役別勝敗 サブタブ（通常 / カオスその他）
     app.js が personalHaiyakuTable に書き込んだ行を2つに振り分ける
══════════════════════════════════════════════════════════════════════════ -->
<script>
function switchHaiyakuTab(tab) {
  const btnN = document.getElementById('haiyakuTabNormal');
  const btnC = document.getElementById('haiyakuTabChaos');
  const panN = document.getElementById('haiyakuPanelNormal');
  const panC = document.getElementById('haiyakuPanelChaos');
  if (!btnN || !btnC || !panN || !panC) return;

  const isNormal = (tab === 'normal');
  panN.style.display = isNormal ? '' : 'none';
  panC.style.display = isNormal ? 'none' : '';

  btnN.style.color       = isNormal ? 'var(--accent)' : 'var(--muted)';
  btnN.style.borderBottom = isNormal ? '2px solid var(--accent)' : '2px solid transparent';
  btnC.style.color       = isNormal ? 'var(--muted)' : 'var(--accent)';
  btnC.style.borderBottom = isNormal ? '2px solid transparent' : '2px solid var(--accent)';
}

(function setupHaiyakuSubTab() {
  const src = document.getElementById('personalHaiyakuTable');
  if (!src) return;

  function distribute() {
    const srcRows = src.querySelector('tbody').querySelectorAll('tr');
    const tbodyN  = document.querySelector('#personalHaiyakuNormalTable tbody');
    const tbodyC  = document.querySelector('#personalHaiyakuChaosTable tbody');
    if (!tbodyN || !tbodyC) return;

    tbodyN.innerHTML = '';
    tbodyC.innerHTML = '';

    srcRows.forEach(row => {
      // 1列目のテキストが配役名
      const haiyaku = (row.cells[0]?.textContent ?? '').trim();
      const clone   = row.cloneNode(true);
      if (haiyaku === 'カオスその他') {
        tbodyC.appendChild(clone);
      } else {
        tbodyN.appendChild(clone);
      }
    });
  }

  // app.js が tbody を書き換えるたびに振り分け直す
  new MutationObserver(distribute)
    .observe(src.querySelector('tbody'), { childList: true, subtree: true });
})();
</script>

<!-- linkwatch: リンクタブの各サイトに更新マークを付ける（/home/mit/linkwatch が生成） -->
<script src="/linkwatch/linkwatch-badge.js?v=<?= @filemtime(__DIR__ . '/../linkwatch/linkwatch-badge.js') ?>"
        data-status="/linkwatch/status.json"
        data-scope="#tab-link"
        data-label="更新" defer></script>

<footer class="site-footer">
  <span>© ＊’サプリの小道具</span>
  <a href="privacy.php">プライバシーポリシー</a>
  <a href="support.php">サポート</a>
  <a href="https://dokasen.co.jp/">運営会社</a>
</footer>

</body>
</html>
