web-todo(認証必須の日次チェックリスト・献立・買い物リスト)

目的

毎朝の決まった行動を当日分のチェックリストとして生成し、完了状態を記録する。あわせて、直近3日分の朝・昼・夜の献立と、買い物の必要・購入済み状態を同じ画面で管理する。

チェックリスト本文は個人的な内容を含むため、公開リポジトリへ初期値・fixture・seed として置かない。認証後の設定画面から登録し、TiDB のみに保存する。

仕様

画面には常に次の順で3項目を表示する。

  1. チェックリスト

  2. 直近の献立リスト

  3. 買い物リスト

買い物が0件なら 買い物リスト:なし と表示する。献立の空欄は 未定 として扱う。

買い物項目は購入済みボタンで打ち消し線を付け、リストへ残したままカゴへ入れた品を判別できる。元に戻すで未購入へ戻せ、不要になった項目は別の削除ボタンで取り除く。同名の品を再追加した場合は数量を更新して未購入へ戻す。

Markdown由来の日次チェックリストとは別に、やるべきことブログネタの2分類を持つ簡単なTODOを表示する。簡単なTODOは日付で複製せず継続リストとして保持するため、未完了項目は翌日以降もそのまま持ち越される。追加、完了チェック、未完了への戻し、削除ができる。

日次チェックリストは前日・翌日ボタン、または/todo?date=YYYY-MM-DDで過去日を表示できる。チェック済み項目の末尾には、設定タイムゾーンでの完了時刻を表示する。過去日に当日分を誤生成しないよう、手動生成ボタンは当日だけ表示する。

親項目をチェックすると配下の子・孫も同じ完了時刻で完了し、親のチェックを外すと配下も未完了へ戻す。子項目を個別に完了しても親は自動完了しない。

/todo/calendarには月間カレンダーを表示する。各日には日次チェックリストの完了数・総数と朝活実績の有無を表示し、日付選択で該当日の/todoへ移動する。未来日は選択できない。

朝活実績は9時を入力目安とする。細切れで発生する育児は時間配分から分離し、育児負荷をなし・軽め・普通・重めの4段階で記録する。別に自由時間を0分・30分・1時間・1.5時間・2時間以上から選び、その主な使い方を怠け中心・運動中心・学習中心・運動+学習から選択する。自由記述は任意とする。10秒以内の入力を目標に、すべてボタン選択で完了できる。アプリ外への通知は行わない。

チェックリスト生成

  • /todo/settings でIANAタイムゾーン、生成時刻、Markdownテンプレートを設定する

  • 入力Markdown原文は規約を含めてDBへ保存する。そのうち # # 寝る前 の箇条書きだけを日次テンプレートへ展開し、インデントを親子関係へ変換する

  • EventBridgeがadmin-api Lambdaを5分ごとに起動する

  • Lambdaはユーザーごとのローカル日付・時刻を計算し、設定時刻を過ぎていれば当日分を生成する

  • user_id + todo_date + source_template_id の一意制約と決定的IDにより、EventBridgeのat-least-once実行でも重複しない

  • 初回設定時は当日分を即時生成する。テンプレート再編集は翌日の生成分から反映する

  • バッチ障害時は /todo の「今日の分を生成」で同じ冪等処理を手動実行できる

認証・非公開性

  • SPA本体にチェックリスト本文は含まれない。静的ファイルが公開されても個人データは取得できない

  • /api/todo とその配下はすべて既存のsessionAuth対象で、Cognitoログインと有効なDBセッションが必要

  • すべての読み書きでセッション由来のuser_idを条件に含め、別ユーザーのIDを指定しても操作できない

  • CloudFrontのHostガードにより、管理画面・APIはadminドメイン以外から露出しない

  • DB保存は暗号化カラムではなく、既存TiDBのアクセス境界で保護する。DB管理者からも秘匿する要件が生じた場合はアプリケーション層暗号化を別タスクで追加する

データモデル

テーブル

用途

todo_settings

タイムゾーン、毎朝の生成時刻、規約を含む入力Markdown原文

todo_template_items

朝・寝る前の階層テンプレート。個人的な本文はここだけに保存

todo_daily_items

日付ごとのスナップショットと完了時刻

todo_meals

日付 × 朝昼夜の献立。未定は行を持たない

todo_shopping_items

買い物と購入済み時刻。同名は正規化して1件にまとめる

todo_quick_items

未完了なら翌日以降も持ち越す簡単なTODO

todo_morning_achievements

日付ごとの育児負荷、自由時間・使い方、自由記述

既存方針に合わせてFKは持たず、ユーザー境界と整合性はAPI層で保証する。

実装箇所

  • apps/admin-web: /todo/todo/calendar/todo/settings、Markdown階層パーサー

  • apps/admin-api: 認証必須のtodo API、日次生成処理

  • iac/aws/lib/admin/admin-stack.ts: 5分間隔のEventBridgeルール

  • tools/dsql-cli/dsl-tidb/schema/12_*.sql19_*.sql: TiDB DDL

検証

実装時に次を実行済み。

bun --filter @shuntaka-dev/admin-api type-check
bun --filter @shuntaka-dev/admin-api test
bun --filter @shuntaka-dev/admin-web build
bun --filter @shuntaka-dev/admin-web test
bun --filter @shuntaka-dev/aws type-check
bun --filter @shuntaka-dev/aws test
  • admin-api: 16 tests passed

  • admin-web Markdown parser: 1 test passed

  • CDK: 9 tests passed、admin stack snapshot更新済み

手動反映手順

DBテーブルが無い状態でadmin stackを先にデプロイすると、5分間隔のバッチが失敗する。DDL → デプロイの順を守る。

1. dev DBへDDL適用

mainへマージするとdev CDKが自動デプロイされるため、マージ前に実施する。

export TAILNET=$(tailscale status --json | jq -r '.MagicDNSSuffix')
cd tools/dsql-cli/dsl-tidb

for file in schema/12_todo_settings.sql \
  schema/13_todo_template_items.sql \
  schema/14_todo_daily_items.sql \
  schema/15_todo_meals.sql \
  schema/16_todo_shopping_items.sql \
  schema/17_todo_quick_items.sql \
  schema/18_todo_morning_achievements.sql \
  schema/19_todo_shopping_items_completed_at.sql; do
  sed 's|${SCHEMA}|blog_dev|g' "$file"
done | mysql -h "tidb.${TAILNET}" -P 4000 -u root -p

mysql -h "tidb.${TAILNET}" -P 4000 -u root -p -D blog_dev \
  -e "SHOW TABLES LIKE 'todo_%'; SHOW COLUMNS FROM todo_shopping_items LIKE 'completed_at';"

7テーブルとcompleted_atカラムが表示されることを確認する。19_todo_shopping_items_completed_at.sqlは既存テーブル向けの差分DDLなので、同じDBへは1回だけ適用する。

2. devデプロイ・動作確認

PRをmainへマージするか、マージ前に手動workflowで検証する。

gh workflow run deploy.yaml --ref <branch> -f stageName=dev -f stack=admin
gh run list --workflow=deploy.yaml --limit 1

デプロイ後に次を確認する。

# APIは未認証で必ず401
curl -s -o /dev/null -w '%{http_code}\n' https://admin.shuntaka.tech/api/todo

# EventBridgeルールが有効
aws events describe-rule --name d-st-todo-generation \
  --query '{State:State,ScheduleExpression:ScheduleExpression}'

ブラウザでhttps://admin.shuntaka.tech/todoへログインし、次を確認する。

  1. /todo/settingsへユーザー保有のチェックリストMarkdownを貼り付ける。本書やGit管理ファイルへ本文を転記しない

  2. 初回保存直後に当日分が生成される

  3. 子項目の階層、チェックON/OFF、完了時刻表示、過去日への移動が動く

  4. 簡単なTODOを2分類で追加でき、未完了項目が日付をまたいでも残り、完了・削除できる

  5. 朝活実績の育児負荷、自由時間、主な使い方、自由記述が数タップで保存できる

  6. カレンダーの月移動、過去日選択、完了件数・朝活実績有無の表示が動く

  7. 献立の保存・未定への戻し、買い物の同名集約・購入済み表示・未購入への戻し・削除が動く

  8. 設定した時刻の次の5分境界以降に、翌日分が1回だけ生成される

DB側の確認は本文を端末へ表示しない集計だけにする。

mysql -h "tidb.${TAILNET}" -P 4000 -u root -p -D blog_dev -e '
SELECT todo_date, period, COUNT(*) AS items, COUNT(completed_at) AS completed
FROM todo_daily_items
GROUP BY todo_date, period
ORDER BY todo_date DESC, period;'

ローカルプレビュー

ローカルではCognitoログインを省略できる。.env.localへ次を追加する。

DEV_AUTH_BYPASS=1
DEV_INSECURE_COOKIES=1

バイパスは、両方のフラグが有効、リクエスト先がlocalhostまたは127.0.0.1、AWS Lambda環境ではない、という全条件を満たす場合だけ有効になる。単一ユーザー運用のためblog_dev.usersの先頭ユーザーを利用する。blog_devへのDDL適用後、別ターミナルで起動する。

bun --filter @shuntaka-dev/admin-api dev
bun --filter @shuntaka-dev/admin-web dev

現在のworktreeのURLはbun run portで確認し、表示されたadmin-web URLの/todoを開く。main worktreeの既定はhttp://localhost:43002/todo

3. 本番バックアップ

本番DDL適用前にリポジトリルートで実行する。

bun run dump:prd

backup/blog_prd-<timestamp>.sqlが作成され、スクリプト末尾の検証が成功したことを確認する。バックアップはgitignore対象であり、コミットしない。

4. blog_prdへDDL適用

tagprリリースPRをマージする前に実施する。

export TAILNET=$(tailscale status --json | jq -r '.MagicDNSSuffix')
cd tools/dsql-cli/dsl-tidb

for file in schema/12_todo_settings.sql \
  schema/13_todo_template_items.sql \
  schema/14_todo_daily_items.sql \
  schema/15_todo_meals.sql \
  schema/16_todo_shopping_items.sql \
  schema/17_todo_quick_items.sql \
  schema/18_todo_morning_achievements.sql \
  schema/19_todo_shopping_items_completed_at.sql; do
  sed 's|${SCHEMA}|blog_prd|g' "$file"
done | mysql -h "tidb.${TAILNET}" -P 4000 -u root -p

mysql -h "tidb.${TAILNET}" -P 4000 -u root -p -D blog_prd \
  -e "SHOW TABLES LIKE 'todo_%'; SHOW COLUMNS FROM todo_shopping_items LIKE 'completed_at';"

5. tagprリリースPRを人間がマージ

mainマージ後にtagprが作成・追従する、tagprラベル付きリリースPRを人間がマージする。手動タグは作らない。

マージによりCalVerタグ作成後、tagpr.yamldeploy-prdがprd CDKを実行し、p-st-adminへ次が反映される。

  • /todo入りadmin-web SPA

  • todo API・バッチ処理入りadmin-api Lambda

  • p-st-todo-generation EventBridgeルール

gh run list --workflow=tagpr.yaml --limit 1

Deploy prd (CDK)が成功してから次へ進む。

6. 本番の初期設定と確認

curl -s -o /dev/null -w '%{http_code}\n' https://admin.shuntaka.dev/api/todo
aws events describe-rule --name p-st-todo-generation \
  --query '{State:State,ScheduleExpression:ScheduleExpression}'
  • 未認証APIが401

  • EventBridgeがENABLEDrate(5 minutes)

  • https://admin.shuntaka.dev/todoがログイン後に表示される

本番の/todo/settingsへチェックリストMarkdownを貼り付けて保存する。dev DBから本文をSQLダンプで移さず、認証画面から明示的に登録する。登録後は本文を表示しない集計クエリで件数だけ確認する。

mysql -h "tidb.${TAILNET}" -P 4000 -u root -p -D blog_prd -e '
SELECT period, COUNT(*) AS templates
FROM todo_template_items
GROUP BY period;
SELECT todo_date, period, COUNT(*) AS items
FROM todo_daily_items
GROUP BY todo_date, period
ORDER BY todo_date DESC, period;'

切り戻し

不具合時はまずEventBridgeルールを止める。チェック状態・献立・買い物データは削除しない。

aws events disable-rule --name p-st-todo-generation

直前の正常なCalVerタグを指定してDeploy workflowのstageName=prdstack=adminを手動実行し、admin stackだけを戻す。テーブルは後方互換な追加のみなので残置する。完全削除が必要になった場合も、先に本番ダンプを取得し、ユーザー確認後に別作業として実施する。

スコープ外

  • チャット本文をAIが解析して献立・買い物へ自動反映する機能

  • DB管理者からも本文を秘匿するアプリケーション層暗号化

  • 複数ユーザー間の共有・権限委譲

  • 通知、リマインダー、連続達成日数などの分析

実施ログ

  • 2026-08-23: bun run dump:prdの初回実行はarticle_embedding_chunks読み出し中の接続切断で失敗したが、再実行で32MBの本番バックアップと全テーブルの件数検証が完了。その後blog_prd.todo_shopping_itemscompleted_at DATETIME(6) NULLを追加し、既存の買い物1件が保持されていることを確認

  • 2026-08-23: blog_dev.todo_shopping_itemscompleted_at DATETIME(6) NULLを追加適用。SHOW COLUMNSで定義を確認し、適用時点の買い物項目は0件だった

  • 2026-08-18: blog_devへtodo用5テーブルをDDL適用。SHOW TABLES LIKE 'todo_%'で全テーブルを確認

  • 2026-08-18: ローカル認証バイパスを追加。ルート.env.localをadmin-apiが直接読むようにし、/api/me/api/todoが200になることを確認

  • 2026-08-18: Playwrightでhttp://localhost:43002/todoを確認。ログイン画面へ遷移せず、チェックリスト・直近の献立・買い物リストの3セクションが表示された

  • 2026-08-18: blog_devtodo_quick_itemsを追加適用。未完了項目を日付に依存せず持ち越す簡単なTODOの保存先を確認

  • 2026-08-18: Playwrightで完了時刻表示、?date=2026-08-17への履歴移動、簡単なTODOの追加・完了・削除を確認。確認用TODOは削除し、日次チェック状態も元へ戻した

  • 2026-08-18: blog_devtodo_morning_achievementsを追加適用。朝活実績の保存先を確認

  • 2026-08-18: 初期の比率入力UIでPlaywright検証後、育児を時間配分から分離する仕様へ変更。確認用レコード1件は条件を限定して削除した

  • 2026-08-18: ユーザー承認後、0件であることを確認したblog_dev.todo_morning_achievementsだけをDROPし、育児負荷・自由時間・主な使い方を持つ最終DDLで再作成。再作成後も0件であることを確認

  • 2026-08-18: Playwrightで朝活実績を重め・1.5時間・運動+学習として保存・再表示し、親チェックによる子項目の一括完了・一括解除を確認。確認用朝活レコードとチェック状態は元へ戻した

  • 2026-08-18: Deploy workflow run 32115530217feat/web-todostageName=devstack=adminで手動起動し、3分6秒で成功。/todo/todo/calendarが200、未認証/api/todoが401、d-st-todo-generationENABLED / rate(5 minutes)であることを確認