| frontend | React + TypeScript + Vite、Tailwind CSS、React Router、Recharts |
| backend | Django + Django REST Framework |
| DB | SQLite(DBコンテナなし) |
| 本番 | 自宅Proxmoxの専用VM(512MB)、Nginx + Gunicorn、Cloudflare Tunnel |
プロフィール編集・ユーザー検索・マスメイト保存の追加APIは 接続仕様 を参照してください。
ホーム・種目選択・おすすめ・記録・結果のAPI接続と未実装範囲は API・データ仕様との対応、フロント所有の種目データは 種目マスター を参照。
必要なもの:Git と Docker Desktop(Windows は WSL2 バックエンド)。Node や Python をPCに入れる必要はない。
git clone https://github.com/jphacks/tkb_2605.git
cd tkb_2605
docker compose up --build- http://localhost:5173 を開き、ログイン画面から新規登録またはログインしてホームへ進む。API・DB接続は http://localhost:5173/api/health/ が
{"status":"ok","database":"ok"}を返すことを確認する。 - 初回はイメージのビルドとnpmのインストールで数分かかる。DBのmigrateは起動時に自動で走る。
- React(
frontend/src)とDjango(backend/)のコードは保存すると自動で反映される。
| やりたいこと | コマンド |
|---|---|
| 起動(バックグラウンド) | docker compose up -d |
| 停止 | docker compose down |
| ログを見る | docker compose logs -f backend(または frontend) |
| 依存を変えた後の再ビルド | docker compose up --build |
| npmパッケージ追加 | docker compose exec frontend npm install <pkg>(package.json と package-lock.json をコミット) |
| Pythonパッケージ追加 | backend/requirements.txt にバージョン固定で追記 → docker compose up --build |
| migrationを作る | docker compose exec backend python manage.py makemigrations |
| 種目マスター投入・更新 | docker compose exec -T backend python manage.py seed_exercises - < frontend/src/features/exercises/serverCatalogData.json |
| 管理ユーザー作成 | docker compose exec backend python manage.py createsuperuser(http://localhost:8000/admin/) |
| テスト | docker compose exec backend python manage.py test |
| DBを空にする | docker compose down -v ⚠ SQLiteのデータが全部消える(node_modulesも消えるので次回起動は遅い) |
DBは sqlite-data というDockerのボリュームに保存されるので、docker compose down や再起動では消えない。消えるのは -v を付けた時だけ。
ホームはユーザアイコンを頭に、2つのトレーニングボタンを胸元に配置し、首・肩のラインの下にタイムラインを表示する。胸元の囲いはボタンの枠だけにし、顔のアイコンは56〜80pxで表示する。上部は180〜224pxの高さを確保してボタンの見切れを防ぎ、通常の縦持ちでは表示領域の約2/3をTLに割り当てる。高さの低い画面ではスクロールして表示する。スマートフォンの下部メニューバーは表示せず、投稿用の+ボタンを画面右下に配置する。幅760pxを超える画面ではスクロール可能なサイドメニューを使用する。
| URL | 画面・操作 |
|---|---|
/ |
投稿一覧APIの常時取得、追加読み込み、いいね、トレーニング開始 |
/exercises |
マスターの種目名検索・部位/器具絞り込み、種目追加API |
/recommendations |
5メニュー・各3候補から選択し、同じ種目追加APIへ接続 |
/workouts/:workoutId/exercises/:exerciseId |
保存済みセット表示、全セット保存、別の種目/終了 |
/workouts/:workoutId/results |
DBの記録による「今日のメニュー」、記録内容を本文に含めた共有、閉じる |
記録フローは Issue #31 の5操作をDBへ接続する。URLは /api/workouts/・/api/workout-exercises/ 配下で、/note/ は付かない。開始・種目追加・セット保存・取得・終了を永続化し、所有者を確認する。Djangoセッション認証と書き込み時のCSRFをフロントから接続済み。開発確認はmigration適用後、上記コマンドで種目を投入し、/loginで登録・ログインする。seedは初回・マスター変更時に手動実行する。種目マスターはフロントのJSONを維持し、名称検索と部位/器具のAND絞り込みをローカルで行う。独立したマスター取得APIは設けない。
投稿作成・投稿詳細・プロフィール・設定・通知は既存URLで利用できる。返信は準備中表示を維持する。記録・結果画面はDB版APIに接続し、保存済みのセットと終了状態を表示する(DB版記録API)。「共有」はPR #30の投稿ダイアログを開き、記録IDと取得済みメニューを渡す。種目名・セット数・各セットの重量と回数を本文に展開し、編集した本文を投稿APIのcontentに含めて送信する。本文全体の上限は500文字で、超過時は編集を促す。添付内容は保存済みの記録で、閉じると結果画面に戻る。投稿APIはDBへ保存し、作成後は保存した投稿の詳細へ進む。記録付き投稿は本人の記録だけを添付できる。既存のログインガードは維持する。
タイムラインは開発環境でも常に Issue #10 の投稿APIから取得し、初回は GET /api/posts/?limit=20&offset=0 を呼ぶ。「もっと見る」では limit=20 を維持し、応答の next_offset をそのまま次の offset に使う。next_offset: null が履歴の終端であり、取得結果が空でも次のoffsetがあれば追加取得できる。取得に失敗した場合は表示済みの投稿とoffsetを維持し、同じ位置から再試行する。重複した post_id は同じページ内・ページ間ともに除去し、表示済み投稿のいいね状態を保持する。
いいねはボディなしの PUT /api/posts/{post_id}/like/ で追加し、応答の like_count と is_liked を表示する。#60の仕様に合わせ、再送しても取り消し・重複は発生しない。いいね済みのボタンは無効になり、初回だけ投稿者へ通知を保存する。詳細は投稿・いいね通知APIを参照。通信失敗時はエラーを表示し、いいねの状態が不明な場合は再読み込みを案内する。投稿・いいねも共通のセッションCookieとCSRF処理を使用する。
ホームは「マスメイト」とベルの2タブ。開始成功時の通知送信・GET履歴・新着表示は通知仕様、端末購読はWeb Pushを参照。ログイン中だけ通知を取得し、ログアウト・アカウント変更で通知と入力状態を破棄する。ホーム・プロフィールには認証APIのIDと名前を使用する。通知・プッシュのバックエンドが未実装でも、トレーニングの記録は続行できる。
セッション認証が必須。本文なしの POST /api/notify/ でログイン中のユーザの通知を保存し、200 と {} を返す。POSTには /api/auth/csrf/ で取得したCSRFトークンを X-CSRFToken ヘッダに付ける。
GET /api/notify/ は全員向けの開始通知と自分宛のいいね通知を対象とし、自分の通知を除外して、前回の取得位置より新しい通知を notified_at 降順(同時刻は notification_id 降順)で最新20件まで配列で返す。?limit=1 ~ 100 で件数を指定でき、不正な値は400、通知なしは []。各要素は notification_id(整数)、notifier_id(整数)、notifier_name(ユーザの user_name)、notified_at(タイムゾーン付きISO 8601日時)。未ログイン時はGET・POSTとも403。
取得位置は notification_cursors テーブルにユーザ単位で保存する。初回GETでは保存済み通知の最新分を返し、以降は新規通知だけを返す。同じユーザの別端末・ログアウト後の再ログインでも返却済み通知は再取得できない。GETの処理時点で取得済みとするため、通信失敗時の再配信はしない。件数上限を超えた古い通知も次回には返さず、取りこぼし対策は行わない。
フロントの開始ボタン連携・1秒間隔のポーリング・通知表示も接続済み。開始通知は従来の4項目を維持し、いいね通知だけ種別・投稿ID・本文抜粋を追加する。通知APIのテストは docker compose exec -T backend python manage.py test notifications。
フロントの契約・状態管理・マスターテスト: docker compose exec -T frontend npm test。
主な動作確認:投稿の取得・ページング・再試行、ログイン後の開始 → 種目追加 → 全セット保存 → 別の種目/終了 → DB記録取得、通信失敗後の手動再試行、連打・離脱後の遷移防止。記録APIは docker compose exec -T backend python manage.py test core.tests で検証する。PC/スマートフォンで各画面の収まりも確認する。静的チェックは docker compose exec -T frontend npm run lint と docker compose exec -T frontend npm run build。
- 保存しても画面が変わらない(Windows):
.envを作ってCHOKIDAR_USEPOLLING=trueを書き、docker compose upし直す。Djangoが反映されない場合はリポジトリをWSLのファイルシステム(\\wsl$\...)側に置く。 - port is already allocated:5173か8000を他のアプリが使っている。そのアプリを止めるか、
compose.yamlの左側のポート番号を変える。 - 「Backend Unreachable」:
docker compose logs backendを見る。たいていは Python の構文エラーかmigrationの失敗。 dependency failed to start: container ... is unhealthy:ビルドではなくバックエンドの起動・ヘルスチェックの失敗。docker compose logs --tail=100 backendで原因を確認する。コード更新後にImportErrorが残っている場合はdocker compose exec backend python manage.py checkを実行し、成功するならdocker compose restart backend→docker compose up -d --waitで復旧する。チェックが失敗する場合は表示されたコード・設定のエラーを先に直す。docker compose down -vはDBを削除するため復旧目的では使わない。npm ciで失敗する:package-lock.jsonがpackage.jsonと合っていない。docker compose exec frontend npm installで作り直してコミットする。
URLは frontend/src/routes.ts、画面の割り当ては frontend/src/App.tsx で管理する。
| 画面 | URL |
|---|---|
| ログイン | /login |
| ホーム・タイムライン | / |
| チャット一覧 | /chats |
| 初心者向けおすすめ | /recommendations |
| 種目選択 | /exercises |
| 種目ごとの記録 | /workouts/:workoutId/exercises/:exerciseId |
| 結果 | /workouts/:workoutId/results |
ログイン以外の上記画面は共通の認証ガードを通す。メールアドレスで登録・ログインでき、起動・再読み込み時はGET /api/users/me/でセッションを復元する。未ログインなら/loginへ移動し、成功後は必ずホーム(/)を表示する。確認中は画面を待機させ、通信障害は再試行できる。DEVプレビューによる認証回避は廃止した。詳細は認証機能の開発を参照。
記録はAPI応答のIDを使用し、workoutExerciseId クエリで応答内の種目を特定する。選択した種目IDと応答が一致することを検証する。「別の種目」では同じ workoutId を引き継ぎ、from クエリに応じて種目選択・おすすめへ戻る。「終了」はセットPOST後にPATCHし、結果をGETする。完了後はGETでも終了状態を確認し、再編集を許可しない。詳細は 記録画面 を参照。
記録IDを認証済みユーザーと混同せず、記録フローではlocalStorage/sessionStorageを使わない。旧 workout.active.v1:・workout.operation.v1:・workout.draft.v1: の値は読み込み・変更・送信しない。入力はメモリだけに保持し、再読込ではAPIの保存済みデータを再取得する。プロフィール編集・ユーザー検索・マスメイト保存は専用APIへ接続する(プロフィールとマスメイト)。投稿・共有の永続化、チャット詳細はバックエンド対応が必要である。
mainからfeature/<内容>ブランチを切る(mainへ直接pushはできない)- コミットしてPull Requestを作る。GitHub Actions の CI が走る
frontend:npm ci・lint・buildbackend:manage.py check・migration漏れの検出(makemigrations --check)・migrate・testcompose:開発用docker compose up --buildが起動し、Vite経由で/api/health/が返るprod:本番用イメージをビルドし、nginx経由でSPAと/api/health/が返る
- CIが全部緑になり、レビューで1人以上のApproveを得たら Squash merge(マージ後ブランチは自動で消える)
mainのCIが通ると、本番用イメージがGHCRにpushされる(ghcr.io/jphacks/tkb_2605-{web,backend}:<commit SHA>)- 本番VMが5分ごとに
mainを確認し、新しいイメージがあれば自動でデプロイする(バックアップ→migrate→起動→health確認。失敗したら前の版に戻す)
依存パッケージの更新はDependabotが毎週月曜にまとめてPRを作る(メジャーアップデートは来ないので、必要なら自分でブランチを切る)。
.github/・ops/・compose.prod.yaml・frontend/nginx.conf を変えるPRは本番に直結するため、オーナーがレビューする(CODEOWNERS)。
構成:cloudflared → web(nginx。Reactのビルド済みファイルを配信し、/api/・/admin/・/static/ を backend へ)→ backend(Gunicorn 1 worker)→ /srv/workout/data/db.sqlite3。
VMはメモリ512MBなのでVM上ではビルドしない。イメージはGitHub Actionsで作り、VMはpullするだけ。
VMへの入り方:Tailscaleで共有を受けて ssh <自分のユーザー名>@jphack-workout(オーナーから招待リンクをもらう)。
| やりたいこと | コマンド(VM上、/srv/workout/app で) |
|---|---|
| 状態・health・メモリ確認 | ops/status.sh |
| ログ | ops/logs.sh backend --since 10m |
| 再起動 | ops/restart.sh(特定のサービスだけなら ops/restart.sh backend) |
| デプロイ | 通常は不要(mainへのマージ後5分以内に自動)。手動なら ops/deploy.sh <mainのcommit SHA>(git pull→イメージpull→バックアップ→migrate→起動→health確認) |
| 自動デプロイの様子 | journalctl -u workout-auto-deploy -n 50 |
| 自動デプロイを止める・再開 | sudo touch /srv/workout/auto-deploy.disabled / sudo rm /srv/workout/auto-deploy.disabled |
| 前の版に戻す | ops/deploy.sh <前のSHA>(自動デプロイは次にmainが進むまでこの版のまま)。migrationでDBが変わっていたら、続けて ops/restore.sh <pre-deployのバックアップ> |
| 手動バックアップ | ops/backup.sh |
| バックアップから戻す | ops/restore.sh /srv/workout/backup/db-....sqlite3.gz |
deploy.sh・backup.sh・restore.shは内部でsudoする(DBファイルと.envがroot・コンテナ所有のため)。- 本番の秘密情報は
/srv/workout/.env(.env.exampleの production 節)。Gitには入れない。 - 自動デプロイ(
ops/auto-deploy.sh、ops/systemd/workout-auto-deploy.timer)は、VMが外から接続を受けない構成のため、GitHubから押し込むのではなくVMが取りに行く。失敗した版は自動で前の版に戻し、同じ版を再試行しない。.envのDEPLOY_NOTIFY_URLにDiscordのWebhook URLを入れると結果が通知される。 - バックアップは1時間ごと(
ops/systemd/workout-backup.timer)、VM内に14世代。R2への暗号化コピーはR2_REMOTEを設定すると有効になる。 - migrationで列を消す・名前を変える変更は、2回のリリースに分ける(先にコードを旧列に依存しない形にしてから消す)。
| 症状 | まずやること |
|---|---|
| サイトが500・応答なし | ops/status.sh → ops/logs.sh backend → ops/restart.sh |
| 直前のデプロイから壊れた | ops/deploy.sh <前のSHA>、またはmainでrevertのPRをマージ(自動で再デプロイされる) |
| データがおかしい | ops/restore.sh <直近のバックアップ> |
| Dockerが動いていない | sudo systemctl restart docker |
| VMにSSHできない・自宅ごと落ちた | オーナーに連絡。急ぐ場合は下の「自宅外への移行」 |
自宅のVMに届かない時、Dockerが動く任意のマシンで同じURLのまま立ち上げられる(Cloudflare Tunnelは同じトークンならどこで動かしても同じホスト名につながる)。
- 自宅のVMが動いている可能性があるなら、先にオーナーにTunnelトークンを再発行してもらう(2か所で同時に動くとアクセスが振り分けられ、DBが2つに分かれる)
- このリポジトリをclone、チームの保管庫から
.envを取り出して置き、DATA_DIR・BACKUP_DIRを手元のパスにする - R2から最新の
*.sqlite3.gz.ageを取り、age -d -i ~/.ssh/id_ed25519 <file> | gunzip > <DATA_DIR>/db.sqlite3 docker login ghcr.io→docker compose -f compose.prod.yaml --env-file .env up -dhttps://<PUBLIC_HOST>/api/health/が{"status":"ok","database":"ok"}を返すことを確認- 自宅が戻ったら、オーナーと相談するまで自宅側は起動しない(移行先のDBを自宅へ戻してから切り替える)
新しいユーザーモデルのコードを取り込む前に実施してください。
この手順は旧ユーザーがなく、note.0001_initial が未適用のDBを対象とします。旧コードで docker compose exec -T backend python manage.py showmigrations と、docker compose exec -T backend python manage.py shell -c "from django.contrib.auth import get_user_model; print(get_user_model().objects.count())" を実行して確認してください。
既存ユーザーはこの手順では新テーブルへ移されません。また、note.0001_initial が適用済みの場合は依存先が auth.User から users.User に変わるため、adminだけ戻しても移行履歴が不整合になります。該当する場合はここから進めず、ユーザー・記録・外部キーを保存する個別の移行が必要です。note zero や --fake で回避しないでください。
操作前にSQLiteのバックアップを取得してください。開発環境では次の例でDBボリューム内に保存できます(同名のバックアップがあれば停止します)。
docker compose exec -T backend python -c "import sqlite3,pathlib; p=pathlib.Path('/data/before-user-model.sqlite3'); assert not p.exists(), 'Backup already exists'; s=sqlite3.connect('/data/db.sqlite3'); d=sqlite3.connect(p); s.backup(d); d.close(); s.close()"docker compose exec backend python manage.py migrate admin zero --plan変更対象が admin だけであることを確認してから実行します。
管理画面の操作履歴は削除されます。
docker compose exec backend python manage.py migrate admin zerousers/migrations/0001_initial.py を含む変更を取り込んでください。
受け取った側で makemigrations を実行する必要はありません。
docker compose exec backend python manage.py migrate users --plan想定外のアプリが含まれていなければ実行します。
docker compose exec backend python manage.py migrate usersdocker compose exec backend python manage.py migrate admin --plan変更対象が admin だけなら実行します。
docker compose exec backend python manage.py migrate admindocker compose exec backend python manage.py check
docker compose exec backend python manage.py showmigrations users adminチェックがエラーなしで終了し、表示されるマイグレーションが
すべて [X] なら完了です。
※ 初めてDBを作る環境では、この切り替え手順は不要です。 ※ すでに新しいコードを取り込んだ場合や、途中でエラーが出た場合は、 そのまま進めず状況を確認してください。 ※ DBファイルやDockerボリューム全体を削除する必要はありません。