現行コード(main)を読み直して起こした、推測なしの仕様。イベントまでの残り時間表示(年次リピート対応)。レビュー用。
最終更新: 2026-07-27 対象: Countdown(resources :countdowns) 関連: Tasks · Timer · Pomodoro
Countdown は、ユーザーが登録したイベント(event_at)までの残り時間をライブ表示する機能。/countdowns にカードを並べ、各カードが残り時間を毎秒更新する。年次リピート(誕生日・記念日)に対応。
title・event_at・recurring・color。next_occurrence が返す。HH:MM:SS(1 時間未満は MM:SS)に切り替わる(#143)。--cd-color でアクセント。空値は既定色へ正規化される(#144)。プラン上限(横断)
機能 上限(Free) Pro Tasks 30(アクティブのみ) 無制限 Habits 3(アクティブのみ) 無制限 Timer 無制限 無制限 Pomodoro 無制限 無制限 Countdown 無制限 無制限 上限の判定は
Billing::PlanLimit.reached?(user, :task)(#173)。Countdown はこの表に載らない側。
| カラム | 型 / 既定 | 制約 |
|---|---|---|
user_id |
bigint, not null | index |
title |
string, not null | presence |
event_at |
datetime, not null | presence(未来検証なし) |
recurring |
boolean, 既定 false | — |
color |
string | 既定 toddyi-priority-fallback / TOKEN_PATTERN。空値は Colorable が既定へ正規化(#144) |
DB CHECK は無し。event_at は UTC 保存、表示はユーザー TZ(#125)。
TOKEN_PATTERN は --cd-color に値を差し込む際の CSS インジェクション境界。Design::Tokens が持つ唯一の定義:
TOKEN_PATTERN = /\A(?:#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))\z/i
つまり 16 進・rgb()/rgba()・hsl()/hsla() のみ。キーワード(red)も var(...) も通らない。Habits と共有(Colorable)。
日付の境界はユーザーのタイムゾーンで決まる(ADR 0005 / #125)。days_remaining は「今日」をユーザーの暦日で数えるので、今夜見た「明日 09:00」は 0 日ではなく 1 日。リクエストは ApplicationController#use_time_zone が包み、スケジュール実行側は各ユーザーのゾーンを自分で適用する。
belongs_to :user。include Colorable(既定色・TOKEN_PATTERN・空値正規化)。next_occurrence(now):非 recurring は event_at。recurring は年の選択と暦日の解決を分け、今年の該当日が now 以降ならそれ、過ぎていれば翌年(#152)。following_occurrence(now):その次の occurrence。クライアントに渡してゼロ到達時のロールオーバーに使う(#145)。非 recurring は nil。seconds_remaining(now) = (next_occurrence - now).floor。days_remaining(now):ユーザーの暦日での差(#143 / ADR 0005)。past?(now) = !recurring? && event_at < now。title/event_at presence。event_at に未来検証は無い(過去イベントは残す方針・#146)。def occurrence_in(year)
month = event_at.month
day = Date.valid_date?(year, month, event_at.day) ? event_at.day : nil
if day
Time.zone.local(year, month, day, event_at.hour, event_at.min, event_at.sec)
else
Time.zone.local(year, 3, 1, event_at.hour, event_at.min, event_at.sec)
end
end
閏日の方針:2/29 の年次リピートは、非閏年では 3/1 に観測する(#152)。以前は change の暗黙のロールオーバーに任せ、rescue ArgumentError で「今年の 2/28」に落としていた。その分岐は過去日でも翌年に補正しなかったため、非閏年の 3 月には負のカウントダウンが翌年まで続いた。年の選択(next_occurrence)と暦日の解決(occurrence_in)を分けたことで、3/1 に解決した結果にも「過去なら翌年」が同じように効く。
DST で存在しないローカル時刻はオフセット分だけ繰り上がる(ActiveSupport の既定)。例外は出ない。
| ルート | アクション | 挙動 |
|---|---|---|
GET /countdowns |
index | @upcoming(recurring と未来の単発・次回発生順)と @past(過ぎた単発・新しい順)に分割(#146) |
GET /countdowns/new |
new | event_at の既定は翌日 09:00(現在時刻だと作った瞬間に過去になる・#146) |
POST /countdowns |
create | save 成功でリダイレクト、失敗 422。過去日なら成功したうえで警告を出す(#146) |
GET /countdowns/:id |
show | 単体表示 |
GET /countdowns/:id/edit / PATCH |
edit/update | see_other |
DELETE /countdowns/:id |
destroy | see_other |
全アクション current_user.countdowns 起点(他人の id は 404)。strong params は params.expect(countdown: [...])。
countdown_controller.js)setInterval は 1 つ(#147)。remaining = floor((Date.parse(eventAt) - now()) / 1000)。now() はサーバー時刻とのズレを補正した現在時刻(config/server_clock・#151)。絶対時刻方式なので背景タブでもズレない。HH:MM:SS。日数はサーバーが計算した暦日を基準に 24 時間単位で減っていく(TZ 正しさを保ったままライブ更新する・#143)。HH:MM:SS、1 時間未満は MM:SS。following_occurrence に切り替える。年の計算はクライアントでは行わない(閏日・DST の解決はサーバー側に留める・#145)。index:@upcoming と @past の 2 セクション。0 件で empty-state。
カード:主表示(日数 or 時計)+副表示ラベル、タイトル、l(occurrence, :long)(recurring は 🔁)、編集/削除。
初期描画はサーバーが整形済みの文字列を出す(#148)。以前は生の秒数(3723 など)を出して JS が置換していたため、一瞬それが見えていた。
<div class="wf__clock">10</div>
<div class="wf__mode">日 · 00:00:00</div>
<div class="wf__t" style="margin-top:.4rem">🚀 Launch Day</div>
<div class="wf__mode">2026年8月6日 09:00</div>
10 日後のイベントは「10 日」。24 時間を切ると時計表示に変わる。
countdowns.*(title/subtitle/new/empty/hhmmss/fields 等)。日時は l(..., :long)。aria-label アイコンボタン。残り時間の変化に aria-live は無い(視覚のみ)。countdowns.today / countdowns.days_ago)。負の時計は表示しない(#145)。| ファイル | カバー範囲 |
|---|---|
test/features/models/planning/countdown_test.rb(11 ケース) |
秒精度/recurring の翌年選択/days_remaining の暦日(#143)/空色の正規化と不正トークンの拒否(#144)/following_occurrence(#145)/閏年春の 2/29 は次の 3/1/非閏年 3 月の 2/29 は次の 2/29/年をまたぐ瞬間の切り替わり/DST 春の飛びで例外を出さない/過去の単発は past? かつ負の残り |
test/controllers/features/planning/countdowns_controller_test.rb(6 ケース) |
CRUD ページ描画/秒精度の作成・更新・削除/他ユーザー 404/index の未来・過去分割(#146)/new の既定 09:00(#146)/過去作成は成功しつつ警告(#146) |
閏日・過去表示・色空値・recurring 通過後は、いずれもカバー済み(#150)。
Toddyi — Countdown Dev 仕様書。コード main 準拠。