diff --git a/docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md b/docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md new file mode 100644 index 0000000..5ece1e3 --- /dev/null +++ b/docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md @@ -0,0 +1,458 @@ +# マスタードキュメント - Windmill通知ワークフロー編 + +> **最終更新**: 2026-02-21 +> **対象システム**: windmill.keinafarm.net(ワークスペース: admins) +> **目的**: このドキュメントだけでWindmill通知ワークフローの全容を把握できること + +--- + +## 目次 + +1. [機能概要](#1-機能概要) +2. [フロー設計](#2-フロー設計) +3. [変更履歴取得API仕様](#3-変更履歴取得api仕様) +4. [LINE通知仕様](#4-line通知仕様) +5. [Windmill設定仕様](#5-windmill設定仕様) +6. [状態管理仕様](#6-状態管理仕様) +7. [設計判断と制約](#7-設計判断と制約) +8. [運用手順](#8-運用手順) +9. [ソースファイル索引](#9-ソースファイル索引) +10. [更新履歴](#更新履歴) + +--- + +## 1. 機能概要 + +### 目的 + +`shiraou.keinafarm.net`(白皇集落営農組合 統合システム)で発生した予約・実績の変更を、LINE Messaging API 経由で管理者に通知する。 + +### ユーザーフロー + +``` +統合システム上で予約・実績の変更が発生 + └→ Windmill が5分毎にポーリング + └→ 変更があればLINEにプッシュ通知 + └→ 管理者がLINEで変更内容を確認 +``` + +### 通知される操作一覧 + +| 区分 | 操作 | 説明 | +|------|------|------| +| 予約 | `create` | 予約が作成された | +| 予約 | `update` | 予約の日時・機械が変更された | +| 予約 | `cancel` | 予約がキャンセルされた | +| 実績 | `create` | 実績が登録された | +| 実績 | `update` | 実績が修正された | +| 実績 | `delete` | 実績が削除された | + +--- + +## 2. フロー設計 + +### Windmillフロー情報 + +| 項目 | 値 | +|------|-----| +| パス | `f/shiraou/shiraou_notification` | +| 概要 | 白皇集落営農 変更通知 | +| ステップ数 | 1(単一Pythonスクリプト) | +| スケジュール | `0 */5 * * * *`(5分毎、JST) | +| スケジュールパス | `f/shiraou/shiraou_notification_every_5min` | + +### 実行フロー(擬似コード) + +```python +# Step 1: シークレット・前回実行時刻を取得 +api_key = get_variable("u/admin/NOTIFICATION_API_KEY") +line_token = get_variable("u/admin/LINE_CHANNEL_ACCESS_TOKEN") +line_to = get_variable("u/admin/LINE_TO") +last_checked = get_variable("u/admin/SHIRAOU_LAST_CHECKED_AT") # 空なら初回 + +since = last_checked or (now() - 10分) + +# Step 2: 変更履歴を取得 +response = GET "https://shiraou.keinafarm.net/reservations/api/changes/?since={since}" + headers: { "X-API-Key": api_key } + +# Step 3: 変更があればLINE通知 +if response.reservations or response.usages: + message = format_message(response) + POST "https://api.line.me/v2/bot/message/push" + body: { "to": line_to, "messages": [{"type": "text", "text": message}] } + +# Step 4: 前回実行時刻を更新(正常完了時のみ) +set_variable("u/admin/SHIRAOU_LAST_CHECKED_AT", response.checked_at) +``` + +### エラー時の挙動 + +- API呼び出し失敗、LINE送信失敗のいずれでも例外が発生 +- 例外が発生した場合、`SHIRAOU_LAST_CHECKED_AT` は更新されない +- 次回実行時に同じ `since` で再試行される(変更の取り漏れ防止) + +--- + +## 3. 変更履歴取得API仕様 + +### エンドポイント + +``` +GET https://shiraou.keinafarm.net/reservations/api/changes/ +``` + +### 認証 + +``` +X-API-Key: +``` + +APIキーが不正な場合は `401 Unauthorized` が返る。 + +### クエリパラメータ + +| パラメータ | 型 | 必須 | 説明 | +|-----------|-----|------|------| +| `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する | + +**`since` の形式例**: +- `2026-02-21T10:00:00+09:00`(タイムゾーン付き、推奨) +- `2026-02-21T10:00:00`(ナイーブ、JSTとして扱われる) + +### レスポンス(200 OK) + +```json +{ + "checked_at": "2026-02-21T12:00:00+09:00", + "since": "2026-02-21T10:00:00+09:00", + "reservations": [ + { + "operation": "create", + "reservation_id": 123, + "user_name": "田中太郎", + "machine_name": "トラクター", + "start_at": "2026-02-25T09:00:00+09:00", + "end_at": "2026-02-25T12:00:00+09:00", + "operated_at": "2026-02-21T11:30:00+09:00", + "operator_name": "田中太郎", + "reason": "" + } + ], + "usages": [ + { + "operation": "update", + "usage_id": 456, + "user_name": "山田次郎", + "machine_name": "コンバイン", + "amount": 4.0, + "unit": "時間", + "start_at": "2026-02-20T08:00:00+09:00", + "end_at": "2026-02-20T12:00:00+09:00", + "operated_at": "2026-02-21T11:55:00+09:00", + "operator_name": "管理者A", + "reason": "記録ミスのため修正" + } + ] +} +``` + +### 変更なし時のレスポンス + +```json +{ + "checked_at": "2026-02-21T12:05:00+09:00", + "since": "2026-02-21T12:00:00+09:00", + "reservations": [], + "usages": [] +} +``` + +### エラーレスポンス + +| ステータス | 原因 | +|-----------|------| +| `401 Unauthorized` | APIキーが不正または未設定 | +| `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 | + +--- + +## 4. LINE通知仕様 + +### 使用API + +LINE Messaging API - Push Message + +``` +POST https://api.line.me/v2/bot/message/push +Authorization: Bearer +Content-Type: application/json +``` + +### リクエストボディ + +```json +{ + "to": "", + "messages": [ + { + "type": "text", + "text": "<フォーマット済みメッセージ>" + } + ] +} +``` + +### メッセージフォーマット + +``` +📋 営農システム 変更通知 + +🟢 予約作成 + 機械: トラクター + 利用者: 田中太郎 + 日時: 2026-02-25 09:00 〜 2026-02-25 12:00 + +🔴 予約キャンセル + 機械: 田植機 + 利用者: 佐藤花子 + 日時: 2026-02-22 08:00 〜 2026-02-22 17:00 + +🔵 実績修正 + 機械: コンバイン + 利用者: 山田次郎 + 利用量: 4.0時間 + 日: 2026-02-20 + 理由: 記録ミスのため修正 +``` + +### アイコン規則 + +| アイコン | 意味 | +|---------|------| +| 🟢 | 作成(create / 予約作成 / 実績登録) | +| 🔵 | 変更(update / 予約変更 / 実績修正) | +| 🔴 | 削除・キャンセル(cancel / delete) | + +### 通知先の種別 + +`LINE_TO` にはユーザーIDまたはグループIDを設定する。 + +| 種別 | ID形式 | +|------|-------| +| ユーザー | `U` で始まる文字列 | +| グループ | `C` で始まる文字列 | + +--- + +## 5. Windmill設定仕様 + +### Windmill Variables(シークレット) + +以下の変数を Windmill UI(Variables ページ)で作成・管理する。 + +| 変数パス | Secret | 説明 | 取得元 | +|---------|--------|------|-------| +| `u/admin/NOTIFICATION_API_KEY` | ✅ | shiraou.keinafarm.net のAPIキー | Djangoサーバー側 `NOTIFICATION_API_KEY` 環境変数と同一値 | +| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | ✅ | LINE Messaging API チャネルアクセストークン | LINE Developers Console | +| `u/admin/LINE_TO` | ✅ | 通知先のLINEユーザーIDまたはグループID | LINE webhook / profile API | +| `u/admin/SHIRAOU_LAST_CHECKED_AT` | ❌ | 前回確認時刻(ワークフローが自動更新) | ワークフローが自動管理(初期値: 空文字) | + +### Django側の設定(shiraou.keinafarm.net) + +`docker-compose.yml` に以下の環境変数を追加: + +```yaml +environment: + - NOTIFICATION_API_KEY= +``` + +APIキー生成コマンド: +```bash +openssl rand -hex 32 +``` + +--- + +## 6. 状態管理仕様 + +### 状態変数: `SHIRAOU_LAST_CHECKED_AT` + +| 項目 | 内容 | +|------|------| +| 格納場所 | Windmill Variable `u/admin/SHIRAOU_LAST_CHECKED_AT` | +| 型 | ISO8601文字列(例: `2026-02-21T15:30:00+09:00`) | +| 初期値 | 空文字(初回実行時は `現在時刻 - 10分` を使用) | +| 更新タイミング | フロー正常完了時のみ、APIレスポンスの `checked_at` を保存 | +| 参照タイミング | フロー実行開始時、`since` パラメータとして使用 | + +### 重複通知防止の仕組み + +``` +実行1: since=T0, checked_at=T1 → LAST_CHECKED_AT = T1 +実行2: since=T1, checked_at=T2 → T1以降の変更のみ取得 +``` + +- `since` に `checked_at`(APIが確認した時刻)を使うことで、変更の取りこぼしが発生しない +- `since`(リクエストに渡した時刻)ではなく `checked_at`(サーバーが確認した時刻)を保存するのがポイント + +### 旧実装との違い(トラブルシュート記録) + +| | 旧実装 | 現実装 | +|---|--------|--------| +| 状態保存方法 | `wmill.get_state()` / `set_state()` | `wmill.get_variable()` / `set_variable()` | +| 問題 | フローのインラインスクリプトでは実行をまたいで保存されない | - | +| 症状 | 毎回 `since = 現在 - 10分` になり、毎回通知が飛ぶ | 正常動作 | + +--- + +## 7. 設計判断と制約 + +### 絶対に変えてはいけない制約 + +1. **`SHIRAOU_LAST_CHECKED_AT` には `checked_at` を保存すること**(`since` を保存しない) + - `checked_at`: APIサーバーが「この時刻まで確認した」という保証付きの時刻 + - 同じ変更が2度通知されることを防ぐ + +2. **状態更新は正常完了後のみ行うこと** + - API呼び出し失敗・LINE送信失敗時は `SHIRAOU_LAST_CHECKED_AT` を更新しない + - 次回実行で同じ範囲を再取得し、通知の取り漏れを防ぐ + +3. **`wmill.get_state()` は使用しないこと** + - Windmillのインラインフロースクリプトでは実行をまたいで保存されない + - 状態管理は必ず Windmill Variable を使うこと + +### 設計判断 + +| 判断 | 理由 | +|------|------| +| 単一ステップフロー | 状態管理を1か所に集約するため。`get_state()`/`set_state()` のスコープ問題を回避 | +| SSL検証スキップ | shiraou.keinafarm.net が自己署名証明書の可能性があるため | +| タイムアウト 30秒 | 農業用途で多少の応答遅延を許容しつつ、無限待機を防ぐ | +| 5分ポーリング間隔 | 農業機械の予約用途では数分の遅延は許容範囲。リアルタイム不要 | + +--- + +## 8. 運用手順 + +### フローを手動実行 + +```bash +curl -sk -X POST \ + -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \ + -H "Content-Type: application/json" \ + -d '{}' \ + "https://windmill.keinafarm.net/api/w/admins/jobs/run/f/f/shiraou/shiraou_notification" +``` + +### 動作確認(curlで直接API呼び出し) + +```bash +# 変更なし確認 +curl -H "X-API-Key: <キー>" \ + "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00" + +# 広い範囲で変更取得(初期確認用) +curl -H "X-API-Key: <キー>" \ + "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00" +``` + +### フローの更新デプロイ手順 + +```bash +cd /path/to/windmill_workflow + +# 1. flows/shiraou_notification.flow.json を編集 + +# 2. 既存フローを削除して再作成(PUTは405のため) +curl -sk -X DELETE \ + -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \ + "https://windmill.keinafarm.net/api/w/admins/flows/delete/f/shiraou/shiraou_notification" + +curl -sk -X POST \ + -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \ + -H "Content-Type: application/json" \ + -d @flows/shiraou_notification.flow.json \ + "https://windmill.keinafarm.net/api/w/admins/flows/create" + +# 3. スケジュールは再作成不要(フローの削除・再作成でも維持される) +``` + +### APIキーローテーション手順 + +1. Djangoサーバー側で新しいキーを生成: `openssl rand -hex 32` +2. `docker-compose.yml` の `NOTIFICATION_API_KEY` を更新してデプロイ +3. Windmill UI で `u/admin/NOTIFICATION_API_KEY` の値を同じ新しいキーに更新 +4. フローを手動実行して動作確認 + +### 過去のジョブ結果確認 + +```bash +curl -sk -H "Authorization: Bearer qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh" \ + "https://windmill.keinafarm.net/api/w/admins/jobs/list?per_page=10&script_path_exact=f/shiraou/shiraou_notification&is_flow=true" +``` + +--- + +## 9. ソースファイル索引 + +### フロー定義 + +| ファイル | 説明 | +|---------|------| +| [flows/shiraou_notification.flow.json](../../flows/shiraou_notification.flow.json) | フロー本体。単一Pythonステップでポーリング・通知・状態更新を実行 | + +**フロー構造**: +```json +{ + "path": "f/shiraou/shiraou_notification", + "value": { + "modules": [ + { + "id": "a", + "value": { + "type": "rawscript", + "language": "python3", + "content": "..." + } + } + ] + } +} +``` + +### ヘルパースクリプト + +| ファイル | 説明 | +|---------|------| +| [wm-api.sh](../../wm-api.sh) | Windmill REST API操作ヘルパー。フロー作成・スケジュール管理に使用 | + +**主要コマンド**: +``` +create-flow JSONファイルからフローを作成 +create-schedule JSONファイルからスケジュールを作成 +flows フロー一覧取得 +schedules スケジュール一覧取得 +``` + +### ドキュメント + +| ファイル | 説明 | +|---------|------| +| [docs/shiraou/19_windmill_通知ワークフロー連携仕様.md](19_windmill_通知ワークフロー連携仕様.md) | 仕様書。API仕様・メッセージフォーマットの原典 | +| [docs/shiraou/20_マスタードキュメント_Windmill通知ワークフロー編.md](20_マスタードキュメント_Windmill通知ワークフロー編.md) | 本ドキュメント | + +### エージェントワークフロー + +| ファイル | 説明 | +|---------|------| +| [.agent/workflows/windmill-push.md](../../.agent/workflows/windmill-push.md) | Windmillへのpush手順。wmill CLIの制限とAPI代替の経緯を記録 | +| [.agent/workflows/windmill-new-script.md](../../.agent/workflows/windmill-new-script.md) | 新規スクリプト作成手順 | + +--- + +## 更新履歴 + +| 日付 | 変更内容 | +|------|---------| +| 2026-02-21 | 初版作成。フロー登録・スケジュール設定・状態管理バグ修正を含む |