最終更新: 2026-07-28。方式・env・エントリポイントは実装(
src/deploy/main.tsの ヘッダコメントが正)に対応。前提の合否は先にdocs/VERIFICATION.mdの §1(deno task verifyほか)を通すこと。
plancel と opulse(現 opulse-monitor)は 1 つの org に軽量2アプリ同居する。
- org は2つ:
torifo(表示名torifo-org、plancel-app+opulse-monitorが同居)とtechub(空・今後の新規用)。 - 旧方式(廃止): 当初は「無料枠の道連れ停止を避けるため org を分離する」方針だった(旧
ADR-2)。個人利用では無料枠の相互停止リスクが小さいと判断し、org 分離はやめた。旧 opulse org
のアプリは削除済み(旧 URL
opulse-monitor.opulse.deno.netは 404)、その org を slugtechubにリネームして転用した。 - 方式は共通: どちらも「console.deno.com の GitHub 連携デプロイ + マネージド KV」。deployctl は使わない。
- リポジトリ: plancel =
torifo/plancel-app(main への push で自動デプロイ)。
無料枠は organization 単位の合算で、超過すると同 org の全アプリが停止する。plancel-app と
opulse-monitor は枠を共有するので、片方の使いすぎがもう片方を止める点に注意。
| 資源 | 無料枠(org 合算) |
|---|---|
| リクエスト | 月 100 万 req |
| CPU 時間 | 15 時間 |
| 帯域 | 100 GB |
| KV ストレージ | 1 GiB |
この枠が §2 の容量設計(開放50 + 保証リスト)の根拠になっている。
-
方式: console.deno.com の GitHub 連携(
mainへの push で自動デプロイ)。ビルドステップ なし(素の Deno、Install/Build コマンドは空)。 -
本番 URL: https://plancel-app.torifo.deno.net (org slug を 2026-07-21 に
plancel→torifoへリネーム済み。旧 URLplancel-app.plancel.deno.netは 404。Google OAuth の承認済みリダイレクト URI もオーナーが 新 URL に更新済み。) -
エントリポイント:
src/deploy/main.ts(統合版)。1 デプロイで全サーフェスを動かす:Deno.serve— 下記のルーティングDeno.cron— 15分毎の境界チェック(plancel-boundary-check)+カレンダー同期の dirty スイープ
-
Deno.serveのルート:パス 用途 GET /,GET /index.htmlWeb UI( web/index.html、MVP の主サーフェス)GET /manifest.webmanifestPWA manifest(インストール名・テーマ・アイコン) GET /sw.jsService Worker(no-cache、明示更新・オフラインshell) GET /icons/*PWAアイコン(192/512px・maskable、ブラウザキャッシュ1日) /auth/*ログイン・マイページ・API トークン・カレンダー/LINE 連携(§4) GET /calendar/<secret>.ics全員向け iCal 購読フィード(§5) POST /api/parse貼り付けメール/画像の取り込み(ログイン必須・401 で拒否) /api/reservations…予約 CRUD + 共有(招待・権限、下記)(ログインまたは API トークン必須) /api/policy-templates…施設の既定規定(下記・台帳ごとに非公開) GET /healthzヘルスチェック( ok <ビルドID>。下記「どのビルドが入っているか」)POST /webhookLINE webhook(LINE env 未設定時は 503) POST /webhook/emailメール転送インテーク(Resend inbound webhook、下記 §3.1) -
予約の共有(招待)と権限(オーナー 2026-07-28「招待する時に編集を許可すれば細かい修正を参加者もできるように」): 予約はオーナーの 台帳に置いたまま参照で共有する(相手の台帳にコピーは作らない)。KV は
["resv_members", <ownerUserId>, <resvId>, <memberUserId>] → { addedAt, role }と 逆引き["shared_in", <memberUserId>, <ownerUserId>, <resvId>] →(同じ値)の2本を常に同一 atomic で更新する。roleはviewer(既定・閲覧のみ) とeditor(内容の編集も可) の2種類で、 role キーを持たない旧行は viewer として読む(既存の共有は読み取り専用のまま)。GET /api/reservations/:id/members— オーナーまたはメンバーが閲覧 →{members:[{userId,displayName,uid,label,role}]}POST /api/reservations/:id/members{q, role?}— 招待(オーナー専用)。role省略はviewer、未知の role は 400、相手が見つからなければ 404user_not_found、自分宛て・重複は 200 の no-op(再招待で権限は変わらない — 変更は下の PATCH で明示的に)PATCH /api/reservations/:id/members/:userId{role}— 権限変更(オーナー専用)→ 200{member}。未知の role は 400、その userId が共有相手でなければ 404DELETE /api/reservations/:id/members/me— メンバー自身が脱退DELETE /api/reservations/:id/members/:userId— オーナーが外す
権限の分界(
src/web/api.tsのmemberGate一箇所で強制): editor に許すのはPATCH /api/reservations/:id(service / startsAt / amount / location / policy / plan)だけ。 確定・キャンセル・復元・削除・招待・権限変更はオーナー専用 — 確定はプラン全体を決着させ (同プランの他候補が自動で要キャンセル)、削除は取り消せないため、参加者に渡した「細かい修正」の 範囲を超える。権限不足のメンバーには 403{error:"forbidden",reason:"role",role}、そもそも共有 されていない相手には 404(予約の存在を教えない)。editor の編集はオーナーの台帳へ直接適用され、 オーナーの Google/ICS を追随させる同じ同期フックが発火する。最後に書いた人は予約のupdated_by(userId、旧レコードは null)に残る。メンバーに見せるオーナー名・相手名はdisplayName → @uid →「共有ユーザー」の順で、メールアドレスは絶対に返さない(招待検索 API と同じ規律)。 -
施設の既定規定(オーナー 2026-07-27「ホテルごとに規定(ベース)を設定できるといいかも」): 施設名をキーに規定を1件だけ覚えておき、同じ宿・同じ店の次の予約で初期値にする。KV は
["web", <ledger>, "policy_tpl", <正規化施設名>](NFKC + trim + 空白畳み + 小文字化。 「ABC」と「ABC」、全角スペースと半角スペースは同一キー)。表示名は入力どおり保存。GET /api/policy-templates— 一覧(表示名順)→{templates:[…]}PUT /api/policy-templates/<施設名>{policy}— 保存・上書き → 200{template}、 不正な規定とunknownは 400DELETE /api/policy-templates/<施設名>— 削除 → 204(存在しなくても 204・冪等)GET /api/policy-templates/lookup?service=<施設名>— フォーム初期値の引き当て →{template}または{template:null}
施設名はパスで percent-encode する。引き当ては正規化後の完全一致のみ(あいまい一致は誤った 料率表を黙って埋めるので入れない)。
unknownはテンプレとして保存できない(規定を「言っていない」 テンプレは無いより悪い)。テンプレは台帳ごとに非公開で、予約を共有しても相手には見えない。 -
cron・webhook・Web は起動時に開く 1 つのマネージド KV(
KvStore.open()= 引数なし)を共有する。 -
cron の通知チャネルは env から自動選択(
selectNotifier): **LINE push(owner宛)> Email(Resend)Console**。
Google ログインは誰でも通せる設計なので、アプリ側でアカウント総数を制限する。
- 開放枠 50(
PLANCEL_MAX_USERS、既定 50): 一般の新規サインアップ上限。既存ユーザーは常に ログイン可、上限で拒否されるのは新規アカウント作成だけ。0で無制限。 - 保証リスト(
PLANCEL_ALLOWED_EMAILS、カンマ区切り): 開放枠が埋まっていても必ず作成できる アドレス。身近な知り合い(~20 想定)を確実に入れるための保険で、開放枠と並行に効く。 - 設計総数 70 = 開放50 + 保証~20。無料枠の実測は ~100 ユーザーまで耐える見込みで、50 は リクエストクォータの半分を余裕として残す意図(org を opulse と共有するため)。
- 超過時の挙動:
- 70 超の新規作成: 開発者向けに
log.warn("user count above design capacity")を残すだけ。 サインアップは止めない。 - 100 到達: 管理者(
PLANCEL_ADMIN_EMAILS)のGET /auth/meにcapacityWarning: trueが付き、 UI が赤いバナーを出す(プールを見直す合図)。
- 70 超の新規作成: 開発者向けに
TOKEN/SECRET/KEY を含む名前は Deploy が自動で secret 扱いにする。
| 変数 | 必須 | 用途 |
|---|---|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
実質必須 | Google OAuth(ログイン + カレンダー連携)。両方揃わないと /auth/google は 503 |
PLANCEL_KEK |
強く推奨 | Google refresh token を封緘する base64 32byte 鍵。未設定だと平文保存(plain: プレフィクス、起動ログで警告) |
PLANCEL_BASE_URL |
任意 | OAuth リダイレクト・マジックリンクの origin を固定。省略時はリクエストの origin |
PLANCEL_MAX_USERS |
任意 | 開放サインアップ上限(既定 50、0 で無制限) |
PLANCEL_ALLOWED_EMAILS |
任意 | 定員を無視して作成できる保証アドレス(カンマ区切り) |
PLANCEL_ADMIN_EMAILS |
任意 | 管理者アドレス。/auth/me に userCount と 100人警告が付く。予約語 uid を取れるのもこのアカウントのみ |
PLANCEL_DEV_USER |
ローカルのみ | OAuth を踏まず、全リクエストをこの email のユーザーとして扱う(本番では設定しない) |
PLANCEL_ADMIN_TOKEN |
任意 | 本番スモーク用。x-plancel-admin ヘッダと一致すれば x-plancel-token を台帳として直接使える |
RESEND_API_KEY / PLANCEL_EMAIL_FROM |
任意 | メールログイン(マジックリンク)の前提。両方揃うと /auth/email が有効化。未設定ならマジックリンクは休眠(コードは残るが送信されない)。RESEND_API_KEY は受信メール本文の読み出し(§3.1)にも必要 |
| 変数 | 必須 | 用途 |
|---|---|---|
GROQ_API_KEY |
◯ | テキスト一次パーサー(Groq) |
GEMINI_API_KEY |
◯ | 条件付き査読 + 画像(Gemini)。「N日前から◯%」型の規定を含むテキストではこれが一次になる。有効枠はAI Studioのプロジェクト/モデル表示を確認し、画像経路と共用 |
LINE_CHANNEL_SECRET |
任意 | webhook 署名検証(未設定なら webhook は 503)。設定済み(2026-07-26 開通) |
LINE_CHANNEL_ACCESS_TOKEN |
任意 | 返信・push・画像取得。長期トークン(Messaging API タブで発行)。設定済み |
LINE_ALLOWED_USER_IDS |
任意 | クローズドベータ用のゲート。公開アカウントでは空(未設定)が正しい — 設定すると、まだ連携していない送信者を全員はじくので、新しい人が連携コードを送ることすらできなくなる。認可はユーザー毎の LINE 連携(§4)なので、連携済みユーザーは絶対にブロックしない。deno task line 単体モードは連携インデックスを持たないため、従来どおりこれが唯一の許可リスト(空なら全拒否) |
PLANCEL_OWNER_USER_ID |
任意 | コア台帳 cron 通知(selectNotifier)の push 先。省略時は許可リストの先頭。台帳の期限リマインドは各ユーザー自身の連携済み LINE へ飛ぶので、この変数は関係ない |
PLANCEL_EMAIL_TO |
任意 | LINE 未設定時の Email 通知先(RESEND_API_KEY + PLANCEL_EMAIL_FROM と合わせて cron 通知の Email フォールバック) |
PLANCEL_EMAIL_DAILY_CAP |
任意 | 期限リマインドの 1日あたり Email 送信数上限(既定 90)。用途: Resend の日100通上限に当てないための自前ガード。到達分は翌日に繰り越し(マーカーを書かないので翌日の sweep で再送)。未設定・0・数値でない値は既定値にフォールバックする(打ち間違いでリマインドが黙って止まらないように) |
RESEND_API_KEY+PLANCEL_EMAIL_FROMはマジックリンク送信と cron の Email 通知を兼ねる。PLANCEL_EMAIL_TOが加わると cron 通知の宛先になる。
期限リマインドの送信枠(オーナー 2026-07-27「知人優先で枠を使いたい」)
台帳の無料キャンセル期限リマインド(src/web/notify.ts、15分 cron)は Resend
無料枠(日100通)の唯一の大口消費者なので、次の2段構えで枠を守る。
- ユーザーごとに1通へバンドル: 同じ sweep で期限を跨いだ予約が複数あっても、送るのは1通だけ (件名は「無料キャンセル期限が近い予約が3件あります」、本文は1予約1行 = サービス/開始/◆無料キャンセル期限/期限後の最大キャンセル料)。冪等マーカーは予約ごとに据え置きで、その1通の送信が 成功した後にまとめて書く(失敗時は1件も書かないので、次の sweep で束ごと再送される)。
- 保証リスト優先: 送信順は
PLANCEL_ALLOWED_EMAILS(保証リスト = 知人)→ それ以外、各グループ内は ユーザーID順。上限に当たった時点で打ち切られるので、知人には必ず先に届いている。 - 日次カウンタは KV
["email_quota", <Asia/Tokyo の YYYY-MM-DD>](expireIn約3日、Email 送信成功 時のみ +1)。LINE 連携済みユーザーは対象外 — LINE push は別クォータで Resend を消費しない。 上限到達時はweb.notifyにwarn("daily email cap reached; deferring to the next JST day")。
予約確認メールを貼り付ける代わりに、転送するだけで自分の台帳に候補として入る
(src/web/email-intake.ts)。Deno Deploy は SMTP を受けられないので、受信は Resend の inbound
webhook 経由。設計の根拠は local/reports/2026-07-27-email-forward-intake.md。
| 変数 | 必須 | 用途 |
|---|---|---|
PLANCEL_INBOUND_DOMAIN |
転送機能を使うなら◯ | 受信ドメイン。本番は Resend が自動発行した ferouhelee.resend.app(独自ドメイン・DNS 設定は不要)。未設定なら /auth/me は mailAddress: null を返し、UI は転送先を出さない |
RESEND_WEBHOOK_SECRET |
転送機能を使うなら◯ | Resend ダッシュボードの Svix signing secret(whsec_…)。未設定なら POST /webhook/email は 503 |
RESEND_API_KEY |
転送機能を使うなら◯ | webhook はメタデータのみなので、本文は Received-Emails API(GET /emails/receiving/{id})で取得する。未設定なら 503 |
PLANCEL_MAIL_DAILY_CAP |
任意 | 1ユーザーが 24 時間に受け付けられる転送数(既定 50)。超過分は 200 を返して何も書かない(転送ループ・アドレス流出時の歯止め) |
ユーザーごとの転送先アドレス: p-<mailSecret>@<PLANCEL_INBOUND_DOMAIN>。mailSecret は
icsSecret と同型の 32byte 乱数で、逆引き索引 ["mail_secret", <secret>] → userId を持つ
(src/web/users.ts)。GET /auth/me が完成形のアドレスを mailAddress で返し、漏洩時は
POST /auth/mail/rotate で発行し直す(旧アドレスは即座に解決しなくなる)。2026-07-28 より前に
作られたアカウントは秘密を持たないので、/auth/me の初回アクセス時に1度だけ発行される。
from は絶対に信用しない: 送信元アドレスは偽装できるので、どの台帳かを決めるのは宛先
ローカルパートの秘密だけ。実在ユーザーのアドレスを騙った from は何も解決しない(ログにだけ残す)。
これに Svix 署名検証(svix-id/svix-timestamp/svix-signature、生ボディに対する HMAC-SHA256、
タイムスタンプのずれ 5 分超はリプレイとして拒否)が重なって、なりすましとエンドポイント直叩きの
両方を止める。
ステータスコードはリトライ契約(Resend は非 2xx を再送する):
| 状況 | 応答 |
|---|---|
RESEND_WEBHOOK_SECRET / RESEND_API_KEY 未設定 |
503 |
| 署名が無い・不一致・タイムスタンプが 5 分超ずれている | 401 |
ボディが JSON でない/email.received の形でない |
400 |
| 生ボディが 64KiB 超 | 413 |
type が email.received 以外(delivered 等) |
200(何もしない) |
| 宛先の秘密がどのユーザーにも一致しない | 200(ログのみ・応答は一致有無を明かさない) |
同じ email_id の再送 |
200(["mail_seen", <email_id>] を atomic に確保・TTL 60日) |
| 1日の上限超過 | 200(ログのみ) |
| AI が読み取れなかった | 200(何も書かず、LINE 連携済みなら「貼り付けてください」と push。メール返信はしない — 送信ドメイン検証が必要で、リマインドの無料枠を食う) |
| 受信本文の取得に失敗(Resend 側の一時障害) | 502(唯一リトライさせたいケース。冪等マーカーは解放する) |
| 登録成功 | 200 |
本文はテキストパートを優先し、HTML のみのメールはタグを除去して使う(上限 100KB)。添付は v1 では
対象外でログに残すだけ。パーサーチェーンは /api/parse・LINE と同じインスタンスを共有し、
登録は HTTP API と同じ createReservation(confirmed: false = 候補)+同じカレンダー同期フック。
メールが規定を書いていなかった場合は施設の既定規定(§1)で補完し、
メールが規定を書いていた場合はテンプレで上書きしない。
オーナーがダッシュボードでやること: ① Resend で受信を有効化(独自ドメインなしなら
*.resend.app が自動割当)→ ② Webhook を https://plancel-app.torifo.deno.net/webhook/email
に向けて email.received を購読し signing secret を控える → ③ Deploy の環境変数に
PLANCEL_INBOUND_DOMAIN と RESEND_WEBHOOK_SECRET を入れて再デプロイ(env 変更は再デプロイまで
効かない)→ ④ 各ユーザーがマイページで自分の転送先アドレスをメールの連絡先に保存。
push と デプロイは別の行為にする。main に push すると GitHub Actions(.github/workflows/verify.yml)が
deno task verify を回し、緑のときだけ production ブランチを main まで進める。Deno Deploy の
Production タイムラインは production を追う。赤なら何も出ない。GitHub の失敗通知が警報になる。
仕組みの要: Deno Deploy の Production タイムラインはリポジトリのデフォルトブランチを追う固定仕様で、
コンソールにブランチ選択は無い。そこで GitHub のデフォルトブランチを production にした(2026-09-14、
gh repo edit torifo/plancel-app --default-branch production)。main は開発ブランチのまま push してよい
("Git Branch" タイムラインがプレビュー URL に出すだけ)。戻すときは同じコマンドで main に。
なぜ: 2026-09-02、1行の宣言順序のバグが本番に出て、全ブラウザで12日間台帳が空だった。捕まえるテスト
(src/web/tests/client_boot_test.ts)は書けるが、push=デプロイのままでは走る場所が無い。
- 起動ビーコン:
web/index.htmlの<body>直後にある独立した<script>。本体が読み込み時に例外を 出すか、8秒たってもwindow.__plancelBootedが立たないと、ログイン不要のPOST /api/beaconに 一度だけ送る。同じ失敗は1日1件だけ保存、全体で1日200件まで。 - LINE 通知:
kind: "system"の報告(カナリア・起動ビーコン)は保存と同時に、PLANCEL_ADMIN_EMAILSのアカウントのうち LINE 連携済みのものへ push する。同じ code は1日1通(月200通の枠を守る)。人の 報告・要望は push しない(マイページ「届いた報告」で読む)。 - 前提: LINE の env と、管理者アカウントが自分の LINE を連携していること(マイページ→連携)。
ログインが唯一の入口。旧方式(ブラウザローカルの匿名トークンが現行)は廃止。旧トークン台帳は
ログイン後 POST /auth/adopt で自分の ledgerId
として一度だけ引き継げる(自分の台帳が空のときのみ)。
- Google OAuth:
/auth/google→ state + PKCE 保存 → 同意画面(scopeopenid email https://www.googleapis.com/auth/calendar.app.created、access_type=offline&prompt=consent)→/auth/callbackで code 交換 → sub/email でユーザー 解決 → refresh token 封緘保存 → セッション Cookie(httpOnly、90日)。 - メール + パスワード:
POST /auth/register(email + パスワード、PBKDF2 ハッシュ)。この経路で 作ったアカウントはメール未検証(emailVerified: false)。POST /auth/loginは email または uid とパスワードで認証(失敗は 15分で10回まで)。 - マジックリンク(休眠):
POST /auth/emailは 15分トークンを Resend で送る設計だが、Resend 未設定のため現状は休眠(503)。コードは残置。 - 未検証アカウントは Google と自動合流しない: 同一アドレスの未検証パスワードアカウントがある
状態で Google ログインすると、乗っ取り防止のため自動マージせず
auth_error=email_takenを返す。 合流は GUI から明示的に Google を「連携」した場合のみ(?link=1)で、リンク成功時に検証済みへ昇格。 - マイページ:
- 表示名(
displayName、重複可) - uid(
POST /auth/profile): 任意・英小文字のみ・一般は 6〜20文字。予約語 (admin/torifo/plancelほか、src/web/users.tsのRESERVED_UIDSが正)は 管理者アカウントのみ取得可(予約語に限り6文字未満も可)。開発者は自分のマイページからadmin/torifoを取る想定。 - 追加ログインメール(
altEmail、例 iCloud) - パスワード設定/変更(
POST /auth/password): Google アカウントにパスワードログインを足せる。 - パーソナル API トークン(
POST/DELETE /auth/token、§6 MCP 用)。 - メール転送先アドレス(2026-07-27、§3.1):
GET /auth/meのmailAddress(p-<mailSecret>@<PLANCEL_INBOUND_DOMAIN>、ドメイン未設定ならnull)。POST /auth/mail/rotateで再発行 → 新しいmailAddressを返し、旧アドレスは即座に無効。 UI 実装済み(2026-07-28): マイページ「メールを転送して予約を取り込む」にアドレス表示・ コピー・再発行の3操作があり、取り込みモーダルの「📨 メールを転送する」を押しても同じアドレスが その場に開く(コピーのみ、再発行はマイページ側)。mailAddressがnull(ドメイン未設定)の環境では 両方の導線が丸ごと隠れる(web/index.htmlのrenderMailFeature)。取り込みモーダル経由の 転送だけは確認画面が無い — 貼り付け・スクショ経路は解析後にプレビューで確認してから登録するが、 転送は読み取れた時点でそのまま候補として台帳に並ぶ。 - LINE 連携(ユーザー毎、2026-07-27):
POST /auth/line/codeが8文字の連携コード (曖昧な O/0/I/l/1 を除く英数字、10分間有効・1回限り、発行し直すと前のコードは即無効)を 返し、それを LINE のトークに送ると連携が完了する。DELETE /auth/lineで解除。GET /auth/meはline: { linked: true|false }だけを返す(LINE userId 自体はクライアントに出さない)。 コード発行は 15分で10回まで(パスワードログインと同じリミッタ・バケットは別)。 LINE のuserIdを身分証明として信用せず、ログイン済みセッションから出したコードだけを 信用する設計。KV:["line_user", <LINE userId>] → userId(逆引き・1 LINE = 最大1アカウント)、["line_link_code", <code>] → { userId, created_at }(expireIn10分)。退会時は両方削除。
- 表示名(
同期対象は確定予約の本体のみ。confirm / 確定済み編集 / cancel / delete をカレンダーへ反映する。
- 全員向け iCal 購読フィード:
GET /calendar/<secret>.ics。confirmed のみ、TZID=Asia/Tokyo、 終了時刻はモデルに無いので一律 +1h、LOCATION対応(会場を渡すとカレンダー側でマップ表示)。 Google/iCloud/TimeTree いずれも購読可。秘密 URL はPOST /auth/ics/rotateでローテートできる。 - Google 連携ユーザー向け:
calendar.app.createdスコープで専用「plancel」カレンダーを自動作成し、 Calendar API で即時プッシュ(iCal のポーリング遅延を埋める)。 - 同期はユーザー操作を絶対に失敗させない(reconcile 型): 予約の現在状態から「確定→upsert / それ以外・削除→イベント削除」を決めるので、再実行・順序前後が無害。
- ⚠ キュー禁止(2026-07-25): Deploy 本番の KV Connect は
kv.enqueue/listenQueue非対応(ローカル KV は対応するためテストをすり抜ける。起動ログに "KV Connect does not support queues")。カレンダー同期はキューを使わず、書き込み時インライン同期 +gcal_dirtyフラグ + 15分 cron スイープで再試行する。invalid_grant(refresh token 失効・取り消し)は恒久エラーと してユーザーをreauth(UI: 要再接続)に落とし再試行しない。今後もキュー前提の設計は本番で 動かないので禁止。
補足: アプリは本番公開済みなので、テスト中アプリにあった「refresh token が7日で失効」の 制約は無い(旧設計の記述)。
Claude Desktop 等から本番の台帳を直接操作できる。
- remote モード: env
PLANCEL_API_URL+PLANCEL_API_TOKENが揃うと、src/mcp/main.tsは ローカル core ストアではなく本番 Web API を叩く 15 ツールを serve する。予約 7 (list / create / confirm / cancel / restore / update / delete)+ 施設の既定規定 4(list_policy_templates/lookup_policy_template/save_policy_template/delete_policy_template→/api/policy-templates)+ 共有 4(list_reservation_members/share_reservation(roleで「編集を許可」)/set_member_role/unshare_reservation→/api/reservations/:id/members)。トークンはマイページの カレンダー連携画面で発行するパーソナル API トークン(x-plancel-token)。Claude で作った予約が 家族の見る台帳に入り、確定はカレンダーにも流れる。 - local core モード: env 無しなら従来どおりローカル KV の core ストアを操作する。
- 原則: 「GUI でできることは MCP でもできる」を目指す。ただしuid 確定・アカウント連携/マージ・ LINE 連携コードの発行は GUI 専用(アイデンティティ確定は人手のみ)で MCP には出さない。 施設の既定規定と共有(招待・権限)はこの例外に当たらないので MCP にも出す(上記8ツール)。 共有ツールも API 側の分界に従うので、オーナー専用の操作は MCP からも実行できない。
Claude Desktop の設定例(claude_desktop_config.json):
{
"mcpServers": {
"plancel": {
"command": "deno",
"args": [
"run",
"--allow-env",
"--allow-net",
"--unstable-temporal",
"--unstable-kv",
"/absolute/path/to/plancel/src/mcp/main.ts"
],
"env": {
"PLANCEL_API_URL": "https://plancel-app.torifo.deno.net",
"PLANCEL_API_TOKEN": "<マイページで発行したトークン>"
}
}
}
}- org:
torifoorg を使う(新規なら作成)。 - プロジェクト作成 + GitHub 連携:
torifo/plancel-appをリンク、Production branch =main。 - エントリポイント指定:
src/deploy/main.ts。Install/Build コマンドは空。 - KV データベース作成 + リンク: プロジェクトに KV(
torifo-kv)を作成/リンクする。 新 Deploy は KV を自動アタッチしない — 未リンクだと起動時にDeno.openKv()が落ちて healthz も返らない。無料プランは org あたり KV インスタンス1つのためplancel-appとopulse-monitorが同一インスタンスに相乗りするが、インスタンス内で app 毎に隔離 DB が切られ データは分離される(ストレージ/読み書きクォータは 2 アプリ合算)。 - 環境変数を設定: §3 の表のとおり。少なくとも
GROQ_API_KEY/GEMINI_API_KEY/GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/PLANCEL_KEK、必要に応じ容量・管理者・LINE 系。 - デプロイ:
mainに push(または「Deploy」実行)。GET .../healthzがok <ビルドID>を返し、ビルドIDが push 前と変わっていることを確認。PWAを更新した場合、sw.jsのCACHE_VERSIONも上げる。利用者はマイページの 「更新を確認」から新しいService Workerを取得し、「今すぐ更新」で待機版を適用できる。 - LINE webhook URL 設定(LINE を使う場合): Messaging API の Webhook URL を
https://plancel-app.torifo.deno.net/webhookにし、「Webhookの利用」をオン、検証を通す。 2026-07-26 設定・検証成功済み(チャネル: プロバイダー plancel / channel 2010848177 / Bot @791wbdma)。 注意: Deploy の env 変更は 再デプロイ(Deploy Default Branch)まで反映されない。 curl での素の POST /webhook は 401(署名なし)が正常。未設定サインは 503。 LINE ConsoleによるWebhook検証までは成功済み。台帳の期限通知、確認・限定更新・追加も コード上は台帳へ統合済みだが、再デプロイ後のLINE実機E2Eは未確認(§9)。 2026-07-27: LINE はユーザー毎連携に移行。PLANCEL_LINE_OWNER_EMAILは削除し(コードは読まない)、LINE_ALLOWED_USER_IDSも空にする(公開アカウントでは空が正しい。設定したままだと未連携の人が 連携コードを送れない)。既に連携済みだったオーナーは、初回だけマイページで連携コードを 発行して LINE に送り直す(旧 env ベースの紐付けは KV に残らないため移行データは無い)。
Deploy はデプロイを識別するヘッダを返さず、/healthz も ok しか言わなかったので、push した
コードが本番に入ったかどうかを外から確かめる手段が無かった(同じ ok がどちらでも返る)。
いまは走っているビルドのIDを返す。
curl -s https://plancel-app.torifo.deno.net/healthz # → ok <ビルドID>- 中身は
DENO_DEPLOY_BUILD_ID(走っているビルド=デプロイごとに変わる)。無ければDENO_DEPLOYMENT_ID(アプリ・ビルド・コンテキスト・環境変数・接続を含むデプロイ設定のID)。 どちらも無いローカルではdev。実装はsrc/lib/build.ts。 - push の前に一度叩いてIDを控え、後で変わったかを見るのが確実。ビルドIDとコミットの対応は Deploy のダッシュボード(console.deno.com/torifo/plancel-app)で確認できる。
- コミットSHAそのものを載せてはいない。ビルド工程を持たない構成では、そのコミット自身のSHAを そのコミットの中に書けない(書けるのは1つ前のSHAで、かえって誤解を招く)。
リッチメニュー本体は LINE 側(LINE Official Account Manager)でオーナー/運用者が作る。 メニューのボタンにできるのは「固定テキストを送る」か「URL を開く」の 2 つだけなので、ボタンに 仕込むテキストは必ず下表のどれかにすること。表にない語は AI 解析に流れ、短文ならコマンド一覧が、 長文なら従来どおり解析結果が返る。
判定は NFKC 正規化 → 前後の空白除去 → ASCII の小文字化 のあとで行うので、全角英字・半角カナ・
大文字小文字・全角スペースは区別しない(src/line/web-commands.ts の COMMANDS が唯一の表)。
| 送るテキスト | 返信 |
|---|---|
確認 / 予定 / 一覧 |
有効な予約一覧(状態ラベル・無料キャンセル期限)+ 次の無料キャンセル期限/最大キャンセル料フッタ + 確定/キャンセル済みの Quick Reply |
締切 / 期限 |
7日以内に無料キャンセル期限が切れる予約のみ(近い順・無料キャンセル期限日と過ぎた場合の最大キャンセル料)。0件なら「ありません」+ 7日より先の次の期限 |
今日 |
JST の今日始まる予約(時刻つき) |
今週 |
JST の今日から7日以内に始まる予約 |
連携 / 設定 |
このトークがつながっている plancel アカウント名+解除手順(未連携なら連携案内) |
使い方 / ヘルプ / help |
サービスの説明・コマンド一覧・予約の追加方法・サイト URL(8行) |
- 返信には必ずコマンドの Quick Reply が付く(
確認/締切/今日/今週/使い方のうち、その 返信自身を除いた 4〜5 個)。リッチメニューが無い状態でもトークだけで回せる。Quick Reply の上限は 13 個なので、確認の予約別ボタンはコマンド分を残した数で打ち切る。 - 未連携の送信者には台帳を一切読まずに連携案内だけを返す(コマンドも解析も走らない)。
- 短文フォールバック: 連携済みの送信者が送った 12文字以下のテキストがどのコマンドにも
一致しなければ、解析にかけず(ParseJob も作らず)コマンド一覧を返す。13文字以上と画像は従来どおり
解析パイプラインへ。しきい値は
MAX_COMMAND_GUESS_CHARS。
LINE の返信からサイトへ誘導するときは、必ず用途ごとのフラグメントを付ける(オリジン直打ちは禁止)。
| フラグメント | 遷移先 |
|---|---|
https://plancel-app.torifo.deno.net/#import |
取り込み(メール本文・スクショの貼り付け) |
https://plancel-app.torifo.deno.net/#link |
マイページの「LINE連携」(連携コード発行・解除) |
https://plancel-app.torifo.deno.net/#help |
使い方 |
web/index.html の routeHash が4経路を処理する。ログイン前はフラグメントを保持し、ログイン後に
対象画面を開いてから履歴から取り除くため、再読み込みで同じモーダルを開き直さない。
- Google Cloud Console「データアクセス」でスコープ
calendar.app.createdを追加。 - OAuth 承認済みリダイレクト URI に
https://plancel-app.torifo.deno.net/auth/callbackを登録。 アプリは本番公開済み(テストユーザー限定・7日失効の段階は終了)。 - (メールログインを使うなら)Resend ドメイン検証 +
RESEND_API_KEY/PLANCEL_EMAIL_FROM。 PLANCEL_KEK(base64 32byte)/PLANCEL_ADMIN_TOKEN/PLANCEL_ADMIN_EMAILSを Deploy env に設定。
再現手順の詳細は docs/VERIFICATION.md §3を参照。
- 認証: Google 実ログイン → 専用「plancel」カレンダーへ確定予約が即時反映 → 削除で同期消去。
- メール+パスワード: 登録 → uid 設定 → uid でログイン。
- MCP: マイページで API トークン発行 → env 設定 → Claude から予約操作が本番台帳に入る。
- cron: Deploy のログで 15分毎の
tick end(failed:0)と、必要時gcal sweep repairedを確認。 - LINE: 未連携のトークに何か送ると連携案内が返る → マイページで連携コード発行 → コードを送って 連携完了(返信に自分のアカウント名が出る)→ テキスト/画像登録が自分の台帳に入る → Quick Replyで確定/キャンセル済み → 期限リマインドが自分のLINEに届く。 2人目のアカウントを連携して、互いの予約が見えないことも確認する。
- Resend: ドメイン検証後に実メールを1通送信。
- Deploy の無料枠が厳しくなったら VPS + SQLite に退避(Store 抽象で経路確保済み。cron は
vps_main.ts= systemd timer run-once)。 - ローカルcore台帳のKVは追記型イベントログが真実の源で、導出値は
verify-projectionで照合できる。 本番の台帳はユーザー別のWebストアとして管理され、coreイベントログとは別モデルである。