現行コード(main)を読み直して起こした、推測なしの仕様。集中/休憩のカウントダウンと集中セッションのサーバー記録。レビュー用。
最終更新: 2026-07-27 対象: PomodoroSession(get/post "pomodoro") 関連: Tasks · Timer · Habits
Pomodoro は、クライアント側で集中/休憩のカウントダウンを回し、集中(focus)が完了したときだけサーバーにセッションを記録する機能。/pomodoro に今日の集中回数・分と直近10件の履歴を表示する。
POST /pomodoro(JSON)。サーバーは kind/duration_seconds/started_at/task_id をクライアントから受け取って記録し、ended_at = Time.current を付す。started_at がユーザー TZ の当日(all_day、#125 以後ユーザー TZ 基準)かつ kind=focus のもの。✅ レビュー対応状況(#128 完了): 下記 §8 の全 finding を修正・マージ済み。 🔴A/nil-started_at=時刻をサーバー権威に(
started_atパラメータ廃止、started_at = ended_at - duration導出、#129)/🟠Bduration_secondsにnumericality {60..5400}(#132)/🟠D 非 bang を checked save+422(#133)/🟠F 冪等client_session_id(#134、||1500も修正)/🟠E long_break 4サイクル+休憩も記録(#135)/task 404+title スナップショット(#137)/localStorage をユーザースコープ(#136)/pending 順序決定化(#138)/背景 drift=絶対時刻+visibilitychange(#130)/音・通知を Start ジェスチャで arm(#131)/レート制限(#140、rack_attack 20/min)。 設計判断: 時刻はサーバー権威・集中時間は自己申告(Q1 (a) 案)。セッションは開始日に帰属。重大度は再評価済み(A🔴維持、F🟠→🔴相当は #134 で解消、E🟢→🟠、H/I/J 🟢→🟠)。
| カラム | 型 / 既定 | 制約 |
|---|---|---|
user_id |
bigint, not null | index (user_id, started_at) |
task_id |
bigint, null | 任意リンク・所有者検証(#49) |
kind |
string, 既定 focus |
CHECK in (focus, short_break, long_break) |
duration_seconds |
integer, 既定 0 |
CHECK ≥ 0(モデル検証なし) |
started_at |
datetime, null | 合成値(#129):サーバーで ended_at − duration_seconds を導出。クライアント時刻は受け取らない=「実際の開始時刻」ではない |
ended_at |
datetime, null | CHECK ended_at ≥ started_at(両者非 null 時) |
CHECK は DB 最終防衛線。started_at/ended_at は null 許容だが、create は常に両方を入れる(started_at はクライアント、ended_at はサーバー)。
belongs_to :user / belongs_to :task, optional: true。KINDS = focus / short_break / long_break。scope focus、today = where(started_at: Time.current.all_day)。task_belongs_to_user のみ(他人の task を弾く、#49)。kind・duration_seconds のモデル検証は無い(DB CHECK 頼み)。minutes = (duration_seconds / 60.0).round(履歴表示用)。| ルート | アクション | 挙動 |
|---|---|---|
GET /pomodoro |
show | 今日の focus 回数/分、直近10件(includes(:task))、pending タスク上位50を用意 |
POST /pomodoro |
create | pomodoro_sessions.create(非 bang)で記録し、今日の集計を JSON で返す |
def create
current_user.pomodoro_sessions.create(
task_id: current_user.tasks.where(id: params[:task_id]).pick(:id), # 他人の task は nil に
kind: KINDS.include?(params[:kind]) ? params[:kind] : "focus",
duration_seconds: params[:duration_seconds].to_i, # 検証なし
started_at: params[:started_at].presence || Time.current, # クライアント供給
ended_at: Time.current)
render json: { count: today_focus.count, minutes: today_focus.sum(:duration_seconds) / 60 }
end
(#128 修正後の挙動) create は checked save:時刻はサーバー導出(started_at = ended_at - duration)、duration_seconds は 60..5400 検証、成功後に集計を返し失敗は 422、`client_session_id` で冪等、他人/未知 task は 404。
リクエスト仕様(明文化): クライアントは JSON body を
fetchで POST(Stimulus)。strong params は通さずparams[...]を直接読む(Tasks の move/toggle と同じ意図的決定:JSON でも form でも受理)。CSRF はfetchがX-CSRF-Token(meta タグ)を付与。POST /pomodoro は rack_attack で 20/min/IP(#140)。
pomodoro_controller.js)setInterval(1000) で remaining を減算。0 で complete()。localStorage に running/mode/endTime/duration/startedAt/taskId/remaining。connect() で復元し、endTime がまだ先なら残り秒を再計算して再開。endTime が過去なら、focus だった場合 log(localStorage.duration || 1500) して break へ。log(seconds) が POST /pomodoro。started_at は最初の start 時の ISO、duration_seconds は 設定した focus 分 × 60(=実測経過ではない)。log() は focus のみ)。complete() は focus↔break を交互。long_break のロジックは無い。beep()(AudioContext 880Hz 250ms)+ notify()(Notification API、許可要求)。モード遷移(クライアント)
ヘッダに「今日 N セッション / M 分」(count/minutes ターゲットを log 応答で更新)。
大きなタイマー表示+トグル(play/pause)+Reset。focus/break 設定(1..90 / 1..60 分)とタスク選択。
履歴(直近10件): kind アイコン(focus=clock / それ以外=coffee)、分、task 名、started_at(%m/%d %H:%M)。
<div class="wf__mode">FOCUS</div>
<div class="wf__clock">25:00</div>
<div class="wf__btns"><span class="wf__btn pri">▶ / ⏸</span><span class="wf__btn">Reset</span></div>
pomodoro.*(title/subtitle/reset/focus_min/break_min/link_task/no_task/sessions_today/minutes_today/recent/notifications など)。aria-label。ただし残り時間・モード変化の aria-live 通知は無い(視覚のみ)。prefers-reduced-motion は該当要素が少なく影響小。重大度: 🔴 重大 🟠 中 🟢 軽微
started_at > ended_at → 500started_at はクライアントの toISOString()、ended_at はサーバーの Time.current。クライアント時計が数秒でも進んでいると started_at > ended_at になり、DB CHECK ended_at ≥ started_at 違反で ActiveRecord::StatementInvalid(500)。create は非 bang だが CHECK 例外は握られない。通常利用(時計ズレ)で到達する 500。対処案: サーバーで started_at = ended_at - duration_seconds を導出、または started_at を ended_at 以下にクランプ/検証。
duration_seconds にモデル検証が無い負値は DB CHECK ≥ 0 で StatementInvalid(500、422 でない)。上限も無く、10 時間の focus も記録可能。Tasks #75 / Habits #118 と同型(モデルと DB の厳しさを揃える)。numericality: { in: 0..(上限) } を。
duration_seconds は「設定した focus 分 × 60」で、実際の経過ではない。決定(Q1 (a) 案): 時刻はサーバー権威にした上で集中時間は自己申告のままとする(2フェーズ化 (b) は個人ツールには過剰)。仕様として「記録時間は自己申告」と明記。範囲は 60..5400 に制限(#132)、スパムは rack_attack で抑止(#140)。
| 機能 | Free | Pro | 増え方 |
|---|---|---|---|
| Tasks | 30(active) | 無制限 | ユーザー操作 |
| Habits | 3(active) | 無制限 | 1/日/習慣(checkin) |
| Timer | 無制限 | 無制限 | ユーザー操作 |
| Pomodoro | 件数上限なし(append-only) | 25分ごと1行・恒久 | |
決定: Pomodoro は件数上限を設けず(履歴が価値)、代わりに rack_attack 20/min/IP(#140)でループ投稿を抑止+ client_session_id 冪等(#134)で二重記録を防止。保持/日次ロールアップは「実測してから」(#83 と同じ measure-first。集計は sum で現状問題なし、行数が問題化したら日次ロールアップ表を検討)。
create が非 bang で保存失敗を無視保存に失敗しても 200+集計を返す。task は所有者フィルタ済みだが、将来検証(B/kind 等)を足したときサイレント失敗になる。集計値も「保存された前提」で返すので実態とズレ得る。create!+適切なエラー応答、または保存結果を見て 422 を返す。
short_break / long_break が未接続enum/CHECK は 3 種だが、クライアントは focus しか記録しない(休憩は log しない、long_break のロジックも無い)。履歴の coffee アイコン分岐も実質未使用。休憩も記録するか、kind を focus のみに絞るか。
connect() のバックグラウンド完了処理は、複数タブや連続リロードで同じ focus を二重に logし得る(サーバー側に冪等キーが無い)。また二重完了時の duration は localStorage.duration || 1500 で、未保存時は実設定を無視して 25 分固定。
履歴の minutes は round、ヘッダ集計は sum/60 の切り捨て。90 秒が履歴で「2分」・集計で「1分」等の不一致。
残り時間・focus↔break の変化に aria-live が無い。完了は音+OS 通知のみ(音の無効化トグルも無い)。
モデル 2 ケース(task 所有者検証)のみ。create(記録・集計 JSON・時刻/duration の扱い)、show、today_focus 集計、クライアントの resume は未テスト。E2E スモークも無い。
Q1. 時刻の権威(🔴A/🟠C): サーバーで started_at = ended_at - duration_seconds を導出してクライアント時刻を信じない方針でよい?(500 も自己申告も同時に解消)
Q2. duration_seconds の検証(🟠B)は 0..N(例: 0..8時間)でよい? 上限値の希望は?
Q3. create を bang 化+422(🟠D)に揃える?
Q4. 休憩(short/long_break)は記録する?(🟠E)それとも kind を focus のみに縮小?
Q5. 二重記録対策(🟠F)は必要?(冪等キー or サーバー側で重複窓を無視)
Q6. コントローラテスト+E2E スモーク(🟠I)を Habits と同様に追加する?
🔴A・🟠B・🟠D あたりを潰すと「サーバー記録が壊れない Pomodoro」になる想定。優先度の指示があれば Issue/サブ Issue 化して着手します。
| ファイル | カバー範囲 |
|---|---|
test/features/models/tracking/pomodoro_session_test.rb |
task 所有者検証(自分の task 可 / 他人の task 不可)の 2 ケースのみ |
未カバー: create(記録内容・時刻/duration の扱い・集計 JSON)、show、today_focus、クライアント resume、E2E。§8🟠I。
Toddyi — Pomodoro Dev 仕様書(レビュー用・noindex)。コード main 準拠。