diff --git a/docs/decisions/0011-shift-notice-dm-only.md b/docs/decisions/0011-shift-notice-dm-only.md new file mode 100644 index 00000000..eb526e96 --- /dev/null +++ b/docs/decisions/0011-shift-notice-dm-only.md @@ -0,0 +1,47 @@ +# 0011: シフト変更の Slack 通知はチャンネルに送らず、本人への DM だけにする + +- 状態:採用 +- 決めた日:不明(2026-04-12 以前の MT。この方針をコードに入れたのは、2026-04-12 のコミット 75649e1) +- 決めた人:45th の SeeFT の MT +- 決定の信頼度:記録なし + - 根拠:コミット 75649e1 のメッセージと差分には、決めたことと「MTの議論により」というコメントがあるが、どれだけ確かだと思っていたかは書かれていない。45th の PM の作業メモにも書かれていない。議論をした MT の資料は、まだ読めていない +- 出典:コミット 75649e1、45th の PM の作業メモ(公開していない) + +## 背景 + +シフトが変わったことを Slack で知らせる仕組みを作るときに、送り先を決める必要があった。チャンネル(委員が全員見る場所)に投稿するか、シフトが変わった本人に DM で送るかである。 + +作りかけのコードは、同じ内容をチャンネルと本人の DM の両方に送っていた(75649e1 の変更前の `SendMessage`)。 + +## 候補 + +| 候補 | 良い点 | 悪い点 | +| --- | --- | --- | +| チャンネルと本人の DM の両方に送る | 誰のシフトが変わったかを、全員が見られる | 約 200 人分の変更がチャンネルに流れ、読まれなくなる | +| チャンネルにだけ送る | 本人の Slack ID を調べなくてよい | 自分に関係する変更を、大量の投稿の中から探すことになる | +| 本人の DM にだけ送る | 自分の変更だけが届く | 本人の Slack ID を DB に入れる必要がある([0012](0012-slack-id-linked-by-gas.md)) | + +## 決定 + +チャンネルには送らず、シフトが変わった本人への DM だけにする。 + +コミット 75649e1 で、`SendMessage` のチャンネルへの送信をコメントにして止め、`NewSlackService` で `SLACK_CHANNEL_ID` を必須にするのをやめた。 + +## 理由 + +MT で議論して決めた。理由は、約 200 人分の変更がチャンネルに流れるとノイズになるから、と PM の作業メモ(2026-04-12 時点)に残っている。 + +コードに残っているのは「MTの議論により、チャンネルへのシフト変更通知は導入しない方針」というコメントだけである。MT の資料そのものは確かめていない。 + +## 前提 + +- 送信:`api/lib/externals/slack/slack_service.go#SlackService.SendMessage`。本人の Slack ID があるときだけ送る +- シフト変更の通知の呼び出し元:`api/lib/usecase/notification_usecase.go#notificationUseCase.processGroup` +- 本人の Slack ID が DB に入っていること([0012](0012-slack-id-linked-by-gas.md)) +- #546(2026-09-20)から、レスキューの DM(`api/lib/externals/slack/slack_service.go#SlackService.SendRescueMessage`)も同じ `SendMessage` を使っている。チャンネルへの送信を戻すときに、コメントを外すだけにすると、レスキューの DM までチャンネルに流れる。戻すなら、シフト変更用の送信を別のメソッドに分け、`api/lib/externals/slack/slack_service.go#NewSlackService` で `SLACK_CHANNEL_ID` を必須に戻す(2026-09-30 に develop のコードで確かめた) + +## 結果 + +45th の技大祭では、シフト変更は本人に DM で届いた。2026-09-18 には、スプレッドシートでセルを空欄にした変更が「(不明)」と表示された DM が届き、問い合わせが来た(#543。表示は PR #544 で直した)。 + +## 追記 diff --git a/docs/decisions/0012-slack-id-linked-by-gas.md b/docs/decisions/0012-slack-id-linked-by-gas.md new file mode 100644 index 00000000..8ce6dae1 --- /dev/null +++ b/docs/decisions/0012-slack-id-linked-by-gas.md @@ -0,0 +1,48 @@ +# 0012: Slack ID の紐付けは GAS の名簿送信で行い、API に一括のバッチは作らない + +- 状態:採用 +- 決めた日:不明(2026 年 8〜9 月ごろ。#476 に設計を書いたのは 2026-08-27、API のバッチを書きかけてやめたのは PR #522 を作っていた 2026-09 中旬) +- 決めた人:45th の PM +- 決定の信頼度:記録なし + - 根拠:#476 と PR #522 の本文には、決めたことと理由は書かれているが、どれだけ確かだと思っていたかは書かれていない +- 出典:#476、PR #522 + +## 背景 + +シフト変更は本人への DM だけで知らせる([0011](0011-shift-notice-dm-only.md))ので、本人の Slack ID(`users.slack_user_id`)が DB に要る。ところが API には、この列を書く処理が無かった(読むだけだった)。Slack のトークンを本番に入れても、DM は誰にも届かない状態だった。 + +Slack ID は、Slack の `users.lookupByEmail` でメールアドレスから引ける。ただ、本番の `users.mail` は 353 人全員が空だった(#476、2026-08-27 時点)。名簿送信の GAS はメールアドレスを送っていたが、API が受け取らずに捨てていた。 + +## 候補 + +| 候補 | 良い点 | 悪い点 | +| --- | --- | --- | +| API(Go)に一括のバッチを作り、DB のメールアドレスから Slack ID を引く | Slack を呼ぶ処理が API の中にまとまる | DB にメールアドレスが無いので、そのままでは引けない。名簿送信とは別に、本番のサーバーでバッチを動かす手順が1つ増える。本番のサーバーを操作する手段は限られていて、コンテナの中でコマンドを動かす運用は重い(PR #522 の本文) | +| GAS の名簿送信で Slack ID を引き、メールアドレスと一緒に API に送る | 名簿送信の1回で、メールアドレスと Slack ID の両方が DB に入る | GAS の6分の実行制限と、Slack の呼び出し回数の制限(1分に50回程度)に当たりうる | + +## 決定 + +GAS の名簿送信で `users.lookupByEmail` を呼び、メールアドレスと Slack ID を名簿のデータに入れて API に送る。API は受け取って保存するだけにする。 + +- 空で送られたときは、DB にある値を消さずに残す。Slack の照会が時間切れで途中までしか引けなかった回や、トークンを入れていない回に送り直しても、消えないようにするためである +- 引けた人はシフトのスプレッドシートの `SlackID` シートに残し、送り直すときは Slack を呼ばない。照会は4分で打ち切り、残りは次の送信で引く +- トークンが無い・間違っているときも、名簿送信そのものは止めない。Slack ID だけを紐付けずに、画面で知らせる + +## 理由 + +API にバッチを置くと、DB にメールアドレスが無いので、先にメールアドレスを入れる手順が要る。名簿送信のほかに、本番でバッチを動かす手順も増える。GAS なら、名簿送信の1回で両方が入る(#476 の「API 側に置かない理由」)。 + +PR #522 を作る途中で Go のバッチを書きかけたが、同じ理由でコミットせずにやめた(PR #522 の本文)。 + +## 前提 + +- GAS 側:`gas/shift/名簿タスク送信.js#attachSlackUserIds_`、`gas/shift/名簿タスク送信.js#lookupSlackUserIdByEmail_` +- API 側:`api/lib/usecase/user_usecase.go#userUseCase.UpdateUsersFromGAS`、`api/lib/internals/repository/user_repository.go#userRepository.UpdateWithSlackUserID` +- GAS のスクリプトプロパティに `SLACK_BOT_TOKEN`(スコープ `users:read.email`)が入っていること +- 本番で通知を有効にするときは、先に API にトークンを入れ、そのあとで名簿送信で紐付ける。逆にすると、溜まっていた過去のシフト変更が一斉に DM で届く(`gas/README.md` の「本番で通知を有効にする順序」) + +## 結果 + +PR #522 を 2026-09-17(日本時間)にマージした。 + +## 追記 diff --git a/docs/decisions/0013-rescue-dm-sent-immediately.md b/docs/decisions/0013-rescue-dm-sent-immediately.md new file mode 100644 index 00000000..7b1b4167 --- /dev/null +++ b/docs/decisions/0013-rescue-dm-sent-immediately.md @@ -0,0 +1,56 @@ +# 0013: レスキューの対応状況の DM は、まとめずに、書き換えのたびにすぐ送る + +- 状態:採用 +- 決めた日:2026-09-20(#545 の本文の「見直した点」) +- 決めた人:45th の PM(メンバーが作っていた PR #546 を引き継いで決めた) +- 決定の信頼度:記録なし + - 根拠:#545 と PR #546 の本文には、決めたことと理由は書かれているが、どれだけ確かだと思っていたかは書かれていない。PR #546 の本文は、実際の Slack での送信はまだ確かめていない(確かめたのはユニットテストと lint まで)と断っている +- 出典:#545、PR #546 + +## 背景 + +レスキューは、委員がアプリから本部に送る問い合わせである。本部が対応状況を変えたり返答を書いたりしたら、送った人に Slack の DM で知らせる機能を作った(#545、PR #546)。 + +最初の案は、PR #546 の最初のコミット(f22d09c)にある。知らせる内容を DB のテーブルに溜め、30秒ごとに見回って、最後の書き換えから15秒たったものを1通にまとめて送る形だった。1通にまとめてほしいという要望が、機能を作ったメンバーのほかから出ていた記録は無い。 + +## 候補 + +| 候補 | 良い点 | 悪い点 | +| --- | --- | --- | +| テーブルに溜め、見回りで1通にまとめて送る | 続けて書き換えても、DM が1通で済む | 届くまでに15〜45秒かかる。確実にまとまるのは15秒以内の書き換えだけ。本番 DB(ほかのシステムと共有しているクラスタ)にテーブルを足す作業が要る | +| 書き換えのたびに、その場で送る | 本部が見たことがすぐ届く。DB を変えなくてよい | 続けて書き換えると、DM が複数届く | + +## 決定 + +書き換えのたびに、その場で DM を送る。送信は API の応答とは別の goroutine で行い、失敗しても送り直さない。返答は、アプリの「本部からの返答」タブでも見られる。 + +あわせて、次のようにした。 + +- 更新の PUT に `notify` を足し、`false` なら送らない。GAS が押し直しで重なったレスキューをまとめるときの書き換えは、`notify: false` を送る。押し直した人に「対応が完了しました」が届かないようにするためである +- 本番に出すときは、GAS を先に、API を後にする。今の API は知らない JSON の項目を無視するので、GAS が先でも害は無い。逆にすると、その間に誤った DM が届く + +## 理由 + +#545 の「見直した点」に、次の理由が書いてある。 + +- レスキューは急ぎで、「本部が見てくれた」は早く届いたほうがよい。まとめるには待つしかなく、待つ分だけ遅れる +- 返答を打つ時間を考えると、15秒以内に続けて書き換える場面は少ない +- レスキューは件数が少なく、シフト変更の通知のように大量の DM が届くおそれがない +- テーブルを足すと、本番 DB への作業が増える + +## 前提 + +- 何を知らせるかの判定と送信:`api/lib/usecase/rescue_notification_usecase.go#rescueNotifier.notify` +- `notify` の扱い:`api/lib/entity/request.go#RescueNotifyOption.ShouldNotify`(省略したときは送る) +- GAS で重なったレスキューをまとめる処理:`gas/rescue/コード.js#closeDuplicateInDb_` +- レスキューの件数が少ないこと。大量に届くようになったら、まとめる形を見直す +- `SLACK_BOT_TOKEN` が無いとき、または `RESCUE_NOTIFICATION_DISABLED=true` のときは送らない(`api/lib/di/di.go#InitializeServer`) +- 送り先は本人の Slack ID で、シフト変更の通知と同じ `SendMessage` を使う([0011](0011-shift-notice-dm-only.md)、[0012](0012-slack-id-linked-by-gas.md)) + +## 結果 + +PR #546 を 2026-09-20(日本時間)にマージした。 + +既知の制約として、同じレスキューへの PUT が1秒未満で重なると、両方が同じ「更新前」の値を読み、DM が1通余分に届くことがある。CodeRabbit はトランザクションで囲むよう指摘したが、本部の書き込みは普通は数秒以上離れるので見送った。直すなら、更新と同時に古い値を返す1つのクエリにする(PR #546 の本文とレビューのスレッド。`api/lib/usecase/question_rescue_usecase.go#questionRescueUseCase.UpdateQuestionRescue` などの3種類の更新)。 + +## 追記 diff --git a/docs/decisions/0014-manual-open-in-new-tab.md b/docs/decisions/0014-manual-open-in-new-tab.md new file mode 100644 index 00000000..cd9fead7 --- /dev/null +++ b/docs/decisions/0014-manual-open-in-new-tab.md @@ -0,0 +1,58 @@ +# 0014: マニュアルはアプリに埋め込まず、別タブで開く + +- 状態:採用 +- 決めた日:2026-08-20(スライド版。#444)、2026-09-13(ドキュメント版も別タブにした。#509) +- 決めた人:45th の PM +- 決定の信頼度:記録なし + - 根拠:#444・PR #451・#509・PR #510 の本文には、決めたことと、埋め込めないことを実測した結果が書かれている。どれだけ確かだと思っていたかは書かれていない +- 出典:#444、PR #451、#509、PR #510 + +## 背景 + +シフトカードとマニュアル一覧から、タスクのマニュアルを開けるようにしている。マニュアルは2種類ある。 + +- ドキュメント版:元の Google ドキュメント。45th の途中から、技大祭の Drive のフォルダに置いた PDF に切り替えた([0016](0016-simple-manual-pdf-on-drive.md)) +- スライド版:SeeFT の API から配信する HTML([0006](0006-manual-serving-google-gate.md)) + +どちらも、閲覧できる人を nutfes の Google アカウントに限っている。 + +最初、ドキュメント版はシフトカードの中に埋め込み(iframe)で表示していた。2026-09-13、ドキュメント版を Drive の PDF に切り替えたあと、埋め込みの枠に Google のエラー画面(401)が出ると報告があった(#509)。 + +## 候補 + +| 候補 | 良い点 | 悪い点 | +| --- | --- | --- | +| アプリの中に埋め込む(iframe やモーダル) | アプリの画面から離れずに読める | 閲覧できる人を限ったファイルは、他サイトの枠の中では開けない(下の「理由」) | +| 別タブで開く | どのブラウザでも開ける | アプリの画面から離れる | + +## 決定 + +マニュアルは、ドキュメント版もスライド版も、埋め込まずに別タブで開く。 + +- スライド版は、最初から別タブで開く作りにした(#444、PR #451) +- ドキュメント版の埋め込み(`ManualViewer`)は、PR #510 で消した + +## 理由 + +閲覧できる人を限ったファイルは、SeeFT の画面の中の枠(他サイトの iframe)では開けない。次のどれか1つに当たると表示されない(#509 の調査、2026-09-13)。 + +- Safari は、他サイトの枠に Cookie を渡さない。Google から見ると、ログインしていない人になる +- Cookie が届いても、ブラウザの既定のアカウントが個人のアカウントだと、権限が無い。枠の中ではアカウントを切り替えられない +- Google のログイン画面は、枠の中に表示できない +- ドライブの `/file/d//view` は `X-Frame-Options: SAMEORIGIN` を返すので、他サイトの枠に表示されない + +URL の形を変えても開けなかった。iPhone の Safari を装い、Cookie なしで他サイトの枠として取得して確かめた(#509、2026-09-13)。結果は、`/document/d//preview` が 302(ログイン画面へ転送)、`/file/d//view` が 403、`/file/d//preview` が 401 だった。 + +スライド版を決めた時点でも、認証の付いたページは埋め込めないことを実測していた(#444 の本文)。サードパーティ Cookie が遮断され、ログイン画面も枠の中に表示されないためである。 + +## 前提 + +- 開く処理:`mobile/lib/widgets/shift_card.dart#_ManualToggleState._open` +- 閲覧できる人を nutfes の Google アカウントに限っていること([0006](0006-manual-serving-google-gate.md))。誰でも読める配信に変えるなら、埋め込みも選べるようになる +- ブラウザが、他サイトの枠に Cookie を渡さない動きを続けていること + +## 結果 + +PR #510 を 2026-09-13 にマージした。 + +## 追記 diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 51d70218..a10a8a14 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -110,3 +110,7 @@ python3 scripts/refcheck/refcheck.py docs/decisions/*.md | [0008](0008-manual-link-by-name.md) | タスクとマニュアルの紐付けは、タスク一覧の URL の列と対応表で行い、キーはマニュアル名にする | 採用 | | [0009](0009-mobile-web-only.md) | mobile は Flutter Web だけで配る | 採用 | | [0010](0010-prod-auto-restart.md) | 本番のサーバーが止まったときに、自動で起動し直すようにする | 提案 | +| [0011](0011-shift-notice-dm-only.md) | シフト変更の Slack 通知はチャンネルに送らず、本人への DM だけにする | 採用 | +| [0012](0012-slack-id-linked-by-gas.md) | Slack ID の紐付けは GAS の名簿送信で行い、API に一括のバッチは作らない | 採用 | +| [0013](0013-rescue-dm-sent-immediately.md) | レスキューの対応状況の DM は、まとめずに、書き換えのたびにすぐ送る | 採用 | +| [0014](0014-manual-open-in-new-tab.md) | マニュアルはアプリに埋め込まず、別タブで開く | 採用 |