# chktime Claude Code の会話ログ(`~/.claude/projects/<プロジェクト>/<セッションID>.jsonl`)を解析して、 タスクごとのやりとりの時刻を表示するローカル Web アプリ。使い方は [README.md](README.md) に書いてある。 ## 構成 ``` chktime.py サーバ + jsonl 解析 + Redmine 連携(これ1本で完結) web/index.html 画面の骨組み web/app.js 表示ロジック(フレームワークなし・素の DOM 操作) web/style.css スタイル(ダークテーマ固定) run.bat コンソール表示で起動 stop.cmd 起動中の chktime を終了 redmine.json Redmine 連携の設定(自動生成・git 管理外) ``` `chktime.py` の中身は上から 「解析ロジック → Store(キャッシュ)/ Watcher(更新検出)→ Redmine 連携 → HTTP」の順。 ## 守ること - **標準ライブラリのみ**。外部パッケージも CDN も使わない。ブラウザ側もフレームワークなし。 - **外部通信は Redmine 連携だけ**。それ以外はローカルの jsonl を読むだけで、どこにも接続しない。 新しい通信先を増やすときは、この行を書き換えるところから始める。 - **API キーをリポジトリに入れない**。`redmine.json` は `.gitignore` 済み。ログにも出さない (`/api/redmine/config` はキーの有無だけ返す)。 - 会話ログは**読み取り専用**。検証で書き換えるときはスクラッチパッドにコピーしてから行う。 - `.bat` / `.cmd` は cmd の既定コードページで読まれるので**中身は ASCII のみ**(日本語を書かない)。 - コメント・コミットメッセージ・UI の文言は日本語。 - 機能を足したら README.md も更新する。 ## 動かす ```bash python chktime.py --port 8799 --no-browser ``` ユーザーは 8787 でデスクトップショートカットから常用しているので、**検証は 8799 など別ポート**で立てる。 停止は `stop.cmd`(ただし `chktime.py` を実行中の python/pythonw を**すべて**終了するので、 検証用だけを止めたいときはコマンドラインでポートを見て個別に kill する)。 ## API | ルート | 内容 | | --- | --- | | `/api/projects` | プロジェクト一覧 | | `/api/sessions?project=` | そのプロジェクトのタスク一覧 | | `/api/all` | 全プロジェクトのタスク一覧(横断モード) | | `/api/session?project=&id=` | 1タスクのターン一覧 | | `/api/daily?project=&days=&from=&to=&exclude=` | 日別集計 | | `/api/timeline?project=&days=` | ガント(タスク単位)。ターン単位は `/api/session` から描く | | `/api/watch` | SSE。ログが変わったら `change` を流す | | `/api/rev` `/api/reload` | 現在の版 / キャッシュ破棄 | いずれも `&format=csv` を付けると CSV を返す(Excel 用に BOM 付き)。 Redmine 連携(POST を使う唯一の系統): | ルート | 内容 | | --- | --- | | `GET/POST /api/redmine/config` | 接続設定の取得(キーは返さない)/ 保存 | | `GET /api/redmine/meta` | プロジェクト・トラッカー・ステータス・自分の情報 | | `POST /api/redmine/draft` | 下書きを作る(`source=tasks` / `source=git`) | | `POST /api/redmine/issues` | 起票をジョブとして開始。返り値の `id` で進捗を見る | | `GET /api/redmine/job?id=` | 起票ジョブの進捗と結果 | ## 解析の前提 - ターンの起点は**人間の発言だけ**。ツール実行結果・システム挿入・サブエージェント(`isSidechain`)は起点にしない。 - 再開時に書かれるメタ行(attachment / system / mode / queue-operation)は**ターンの所要時間に数えない**。 セッション全体の開始・終了時刻には反映する。 - `tool_result` を含む行は巨大なので JSON パースせず、正規表現で `timestamp` だけ拾う。 - 解析結果は `(mtime, size)` でキャッシュし、追記されただけのファイルは**増えたぶんだけ**読み足す (`SessionParser.read()`)。書きかけの行は消費せず次回に回す。先頭と解析済み末尾のバイト列が 変わっていたら書き換えとみなして頭から読み直す。 - 追記解析は状態を持つので、同じファイルを複数スレッドで同時に解析しない(`Entry.lock`)。 ## 画面 - 3ペイン(プロジェクト / タスク一覧 / 詳細)。右ペインは **やりとり・日別集計・ガント**のタブ。 - 状態はすべて `app.js` 冒頭の `state` オブジェクト。再描画は `render*()` が `#detail-body` を作り直す方式。 - 自動追従(SSE)で作り直すときも、選択・スクロール位置・絞り込み・タブは保つ(`refreshFollow()`)。 - ガントの軸・背景・枠組みは `timeScale` / `axisSvg` / `gridSvg` / `ganttFrame` をタスク単位と ターン単位で共有している。 - `/api/timeline` は表示範囲より前から続くセッションの全体時間・往復数・ツール数も範囲内に切り詰め、 `clipped` を立てて返す(作業時間だけ切り詰めると数字が食い違うため)。 - Redmine 起票のダイアログは `rm` オブジェクトが状態を持ち、`renderRedmine()` が中身を作り直す。 HTML 属性に NUL は入れられない(パーサが置換する)ので、タスクの識別は `data-project` / `data-id` の 2 属性で持つ。 ## Redmine 連携の落とし穴 実際に踏んだもの。同じところで転ばないように。 - **作成時の `status_id` は無視される**(既定の New で入る)。状態を指定したいなら作成後に `PUT /issues/.json` で付け直す(`Redmine.create()` がやっている)。 - **連続アクセスに 403 を返すサーバがある**。7〜8 秒あければ通る。`Redmine.throttle()` が クライアント単位で間隔を保つので、クライアントは作り直さず使い回す。 - **自己署名証明書**が普通なので、既定は検証なし(`verifySsl: false`)。 - 起票は件数×間隔ぶん時間がかかるので HTTP を掴んだままにしない。`Jobs` に投げて進捗を取りに来てもらう。