訪問看護師が1日5〜10件の訪問先でストップウォッチ的に滞在時間を記録するWebアプリ。タップ1回で開始・終了を記録し、日々のデータをクラウドに蓄積する。
訪問看護の現場では「いつ・どこで・何分訪問したか」を正確に記録することが、診療報酬の根拠になる。既存の紙記録や汎用アプリでは不十分で、GPS情報つきの時刻記録を改ざん困難な形で残せる仕組みが必要だった。
将来的に診療報酬改定で「GPS記録による適正訪問の証明」が加算要件になる可能性も見据えて、今のうちからデータを蓄積しておく目的もある。
| フロントエンド | 素のHTML / CSS / JavaScript(フレームワークなし) |
| バックエンド / DB | Supabase(PostgreSQL + Auth + Edge Functions) |
| ホスティング | Cloudflare Pages |
| ソースコード管理 | GitHub(Private) |
| メール通知 | Resend |
| 開発ツール | Claude Code |
ネットワーク版から入るとSupabaseの設計・認証・RLSなどを一度に考える必要があって複雑になる。まず手元で動くものを作ってUIと操作感を確認する、という順序で進めた。
ブラウザに内蔵されたローカルデータベース。サーバー不要でデータを保存できる。ページをリロードしてもデータが消えない。
// データ構造(ローカル版)
DailyRecord {
date: "YYYY-MM-DD", // Primary Key
visits: Visit[],
createdAt: string
}
Visit {
id: crypto.randomUUID(),
startTime: string | null,
endTime: string | null,
duration: number | null // 秒
}
IndexedDBはそのブラウザ・その端末にしかデータがない。複数人での運用、端末の切り替え、データの共有ができない。ネットワーク版への移行が必要だった。
IndexedDBからSupabase(PostgreSQL)へ移行。認証・RLS(行レベルセキュリティ)・リアルタイムDB・Edge Functionsが一体になったBaaSで、個人〜小規模チームには過不足なくちょうどいい。
| テーブル | 内容 |
|---|---|
profiles | 職員情報(name, role, approved) |
visits | 訪問記録(date, staff_id, visits JSON, memo等) |
patients | 利用者マスタ(name, active) |
ネットワーク版を公開してすぐ、Chrome Mac環境でログイン後にアプリ画面が表示されない問題が発覚した。Safariでは動く。
DevToolsで直接fetchを叩いてみると即座にレスポンスが返ってくる。つまりネットワーク自体は問題ない。
// DevToolsで試したら即返ってきた → ネットワークは正常
fetch('https://xxx.supabase.co/rest/v1/profiles', {
headers: { 'apikey': '...', 'Authorization': 'Bearer ...' }
})
// → OK 200
調査の結果、Supabase JS SDKが内部で使っているWeb Locks API(navigator.locks)がChrome Macで特定条件下で永久にブロックされることが原因と判明。
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
auth: {
lock: async (name, acquireTimeout, fn) => fn() // ロック無効化
}
});
これでログインは通るようになったが、今度はログイン後のデータ取得がハングした。SDKのINITIAL_SESSIONトークンリフレッシュ処理も同様にロックを使っていたため。
SDKのデータ操作メソッドを全廃し、素のfetchでSupabase REST APIを直接叩くヘルパー関数を作った。
async function apiFetch(method, table, params = '', body = null, extraHeaders = {}) {
const token = currentSession?.access_token;
const headers = {
'apikey': SUPABASE_ANON,
'Content-Type': 'application/json',
'Accept': 'application/json',
...extraHeaders
};
if (token) headers['Authorization'] = `Bearer ${token}`;
const url = `${SUPABASE_URL}/rest/v1/${table}${params ? '?' + params : ''}`;
const opts = { method, headers };
if (body !== null) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
if (res.status === 204 || res.status === 201) {
const text = await res.text();
return text ? JSON.parse(text) : null;
}
return res.json();
}
SDKは便利だが、内部実装に依存した罠がある。「動かない」と思ったらまずraw fetchで疎通確認。ネットワークが正常ならSDKの問題を疑う。
各訪問カードにメモ欄を追加。また完了済み訪問の開始・終了時刻とメモを後から修正できる編集ダイアログを実装した。
「時刻の後修正を許すべきか」は議論があった。不正防止の観点では禁止したほうがいいが、入力ミスの修正ニーズもある。現状は編集可能にしつつ、将来の仕様検討事項としてTODOに残している。
日本語変換(IME)の確定前にEnterキーが反応してしまい、未確定の文字がそのまま入力される問題を修正。
// compositionstart / compositionend で変換中かどうかを追跡
input.addEventListener('compositionstart', () => { isComposing = true; });
input.addEventListener('compositionend', () => { isComposing = false; });
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !isComposing) commitLabel();
});
以前は編集時にもGPSを再取得していた。記録の証拠性を高めるため、GPSは最初の「開始」ボタンを押した時のみ取得し、編集では上書きしない仕様に変更。
// 既にlocationが記録済みなら取得しない
if (!v.location) {
navigator.geolocation.getCurrentPosition(pos => { ... });
}
以前は全記録がフラットに並んでいた。月ごとにグループ化し、クリックで開閉できるアコーディオン形式に変更。件数・合計時間もサマリー表示。
集計ビューに「全期間 / 月別」フィルターを追加。各集計カードをクリックすると日別の詳細データが展開表示される。
利用者別・職員別の集計データをCSVでエクスポート。ExcelでそのままUTF-8で開けるようBOM付きで出力。
| CSVの種類 | 含まれる項目 |
|---|---|
| 利用者別 | 日付、曜日、職員名、利用者名、開始/終了時刻、時間、住所、緯度、経度、精度、メモ |
| 職員別 | 日付、曜日、職員名、訪問件数、合計時間、利用者名一覧 |
それまでNetlifyのドラッグ&ドロップデプロイを使っていた。これが1回10クレジット消費する仕組みで、開発中に繰り返しデプロイするうちに無料枠(100クレジット)をほぼ使い切ってしまった。
ドラッグ&ドロップをやめてGitHubと連携した自動デプロイに切り替える。git pushするとNetlifyが自動でデプロイしてくれる仕組みで、この方式ならクレジットを消費しない。
# Gitリポジトリを初期化
cd "/Users/yuzosaito/App Demo/訪看訪問時間管理"
git init
git add index.html app.js style.css CHANGELOG.md .gitignore
git commit -m "初回コミット: 訪問記録APP Ver2.2"
# GitHubリポジトリ(homebisit-app)を作成後
git remote add origin https://github.com/UzAvie/homebisit-app.git
git branch -M main
git push -u origin main
認証にはPersonal Access Tokenを使い、macOSキーチェーンに保存して以降は自動認証。
# コードを修正したら
git add index.html app.js style.css
git commit -m "変更内容のメモ"
git push
# → GitHubに届いたのをNetlifyが検知して自動デプロイ(約4秒)
コミットメッセージはCHANGELOGと連動させるとあとで追いやすい。git add -Aより特定ファイル名でgit addする方が意図しないファイルの混入を防げる。
それまでアプリフォルダの外に置いていたTODO.mdをリポジトリ内に移動。CHANGELOG.mdと並んで同じGitリポジトリで管理するようにした。
職員が新規登録すると管理者に通知メールが届く仕組みを実装。それまでは管理者が定期的に⚙モーダルを開いて確認するしかなかった。
| サービス | 役割 |
|---|---|
| Resend | メール送信API(月3,000通まで無料) |
| Supabase Edge Functions | メール送信処理(Deno/TypeScript) |
| Supabase Database Webhooks | profilesテーブルへのINSERTを検知してFunctionを起動 |
新規登録される
→ profiles テーブルに INSERT
→ Database Webhook が発火(on_new_profile)
→ Edge Function(notify-new-signup)が呼ばれる
→ Resend API でメール送信
→ 管理者のGmailに届く
Deno.serve(async (req) => {
const payload = await req.json();
const record = payload.record;
const name = record?.name ?? "(名前未設定)";
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${Deno.env.get("RESEND_API_KEY")}`,
},
body: JSON.stringify({
from: "onboarding@resend.dev",
to: Deno.env.get("ADMIN_EMAIL"),
subject: "【訪問記録APP】新規登録があります",
html: `${name} さんが登録申請しました。
`
}),
});
return new Response(JSON.stringify({ ok: true }), { status: 200 });
});
APIキーや管理者メールアドレスは Supabase の Edge Function Secrets(環境変数)に保存し、コードには直書きしない。
Netlifyのクレジットが残り30を切った(自動デプロイ移行後も初回設定で消費)ため、Cloudflare Pagesへ完全移行した。
| Netlify(旧) | Cloudflare Pages(新) | |
|---|---|---|
| 無料デプロイ数 | 100クレジット(消耗品) | 500ビルド/月(毎月リセット) |
| 帯域 | 100GB/月 | 無制限 |
| GitHub連携 | ○ | ○ |
| デプロイ速度 | 約4秒 | 同等 |
移行作業はGitHubリポジトリをそのまま使いまわせるため、Cloudflare Pagesの管理画面から homebisit-app を選んで接続するだけで完了した。コードの変更は不要。
| サービス | 役割 | 保存するもの |
|---|---|---|
| GitHub | コード管理 | ソースコード・変更履歴 |
| Cloudflare Pages | 公開・配信 | HTML / CSS / JS(コードのコピー) |
| Supabase | データ管理・認証 | 訪問記録・職員・利用者 |
| Resend | メール送信 | — |
【開発時】
PCでコードを修正
→ git push
→ GitHubにコードが保存
→ Cloudflare Pagesが自動検知
→ デプロイ完了(数秒)
【利用時】
スマホ・PCでアプリを開く
→ Cloudflare PagesがHTML/JSを配信
→ ブラウザでアプリが動く
→ 操作のたびにSupabaseと通信
→ データが保存・取得される
コード(GitHub + Cloudflare)とデータ(Supabase)は別の場所にある。コードをいくら変えてもデータは消えないし、データが増えてもコードは変わらない。これがクラウドネイティブな設計の基本。
従来型はサーバー1台にHTML・DB・メールが全部同居していた。今の構成は役割ごとに専門サービスが分担する「モダン分散型」。初期費用ゼロ・メンテナンス不要・スケーラブルが特徴。小規模なら全部無料で動く。
Chrome Macのハング問題は「ネットワークが悪い」と思いがちだが、原因はSDK内部のWeb Locks APIだった。raw fetchで疎通確認するという診断手順が身についた。
Git導入はVer2.3からだったが、もっと早い段階から入れておけばよかった。git initのコストはゼロなので、プロジェクト開始時点でGitを入れるのが正解。
何をどのバージョンで実装したかをCHANGELOG.mdに残し、次にやることをTODO.mdに整理する。これがないと「前回何やったっけ」が発生する。Claude Codeとの作業でも文脈が引き継ぎやすくなる。
Netlifyは無料枠の制限が厳しくなっている。新規プロジェクトは最初からCloudflare Pagesにしておくほうが無難。
| カテゴリ | 内容 |
|---|---|
| 機能追加 | 会議・外来同行・オンコール対応などの業務タイプを追加 |
| 機能追加 | 利用者情報の拡張(対象/対象外フラグ、住まい、GAFスコア等) |
| 仕様検討 | 時刻の後修正を禁止する方向での検討(不正防止) |
| 仕様検討 | 管理者権限の多段階化(全権/副管理者/一般) |
| インフラ | 独自ドメイン設定 |
| 将来対応 | 診療報酬改定を見据えたGPS証明の仕組み整備 |
| アプリURL | https://homebisit-app.pages.dev |
| GitHubリポジトリ | https://github.com/UzAvie/homebisit-app |
| コード場所(ローカル) | /Users/yuzosaito/App Demo/訪看訪問時間管理/ |