2026-08-04 letip/salesstock/stocklist: 販売品在庫リストで Z在庫が 0 になる不具合の調査と修正 現象: 販売品在庫リスト(letip/salesstock/stocklist/)で JAN 4987601512995 / 商品コード A610002 (packno=101500、棚「販売品別置(M川崎)」=MKW)の 2026年7月分が、 期首在庫1・期末在庫1 に対して Z在庫だけ 0 と表示された。 調査: - 過去月の Z在庫は pack_archive の「翌月1日」スナップショット(divisioncode='2026/08/01') から取得している(stocklist/index.php:273)。そこに当該行が存在しないため 0 になっていた。 - 現在庫テーブル pack には qty=1 で存在。7月の tasklog(入出庫)は 0 件で、 期首1・期末1 という計算結果自体は正しい。 原因: pack テーブル 9,287行のうちこの1行だけ divisioncode に値('2024/09/26')が入っていた。 日次バックアップ letip/packbackup.php(毎日 0:00 cron)は REPLACE INTO pack_archive SELECT * FROM pack; UPDATE pack_archive SET divisioncode='当日' WHERE divisioncode=''; という作りで、「pack.divisioncode は必ず空」という前提に依存している。 値が入った行は当日バケツではなく '2024/09/26' というバケツへ退避され続け、 日次スナップショットから静かに欠落していた(1年以上前から継続)。 ※ 毎月1日のスナップショットにだけ載っていたのは、担当者が手作業で補正していたため(ユーザー確認済み)。 2026-08-01 はその手作業が行われず、今回初めて表示不整合として顕在化した。 対応(データ): 1. UPDATE pack SET divisioncode='' WHERE packno=101500(異常値のクリア) 2. pack_archive の 2026/08/01〜08/04 に当該行を補完(REPLACE + UPDATE、各文 0.1秒以内) 3. 誤バケツ '2024/09/26' の残骸は日次 cleanup で消える(pack 側をクリア済みのため再生成されない) ※ 7/02〜7/31 の日次スナップショットには欠落が残るが、在庫表は各月1日しか参照しないため影響なし。 対応(コード): letip/packbackup.php を修正(ローカルのみ・未デプロイ) 一時テーブル pack_snapshot へ pack を複製 → divisioncode を当日日付で一括確定 → pack_archive へ REPLACE。 pack 本体は一切変更せず、divisioncode の値に関係なく全行が退避されるようにした。 あわせて「pack の件数」と「当日スナップショットの件数」を照合し、 不一致なら packbackup.log に WARNING を出すようにした(欠落の早期発見のため)。 → この修正により、月初に行っていた手作業の補正は不要になる。 【インシデント】調査中に本番 MySQL を停止させた(14:00頃〜14:16、約25分) 経緯: 調査のため pack_archive(MyISAM・590万行・35GB)に自己結合クエリを実行。 MySQL 5.0 は1テーブル1索引しか使えず (divisioncode, packno) の複合索引も無いため プランが崩れ、1,485秒「Sending data」のまま滞留してテーブル読み込みロックを保持。 さらにその後 REPLACE INTO pack_archive を投げたため書き込み待ち行列ができ、 在庫表を含む pack_archive 参照処理が全て停止した。 誤り: クエリのタイムアウト後、ローカルの php プロセスを停止しただけで「止めた」と誤認した。 クライアントを切ってもサーバー側のクエリは走り続けるため、 SHOW FULL PROCESSLIST → KILL でサーバー側を止める必要があった。 復旧: 該当クエリ2件(Id 317604, 317690)を KILL。ロック解放を確認。データ破損は無し (pack の divisioncode クリアは完了済み、pack_archive に中途半端な行は残っていなかった)。 再発防止: 本番DBへの調査クエリは索引の効く単一条件に限定し、結合・全表走査は投げない。 必要な差分はアプリ側(PHP)で突き合わせる。書き込みは影響行を限定した最小の文で行う。 【今後の検討事項】pack_archive の肥大化 9,000行/日の全件スナップショットを無期限に蓄積しており、現在 590万行・35GB (間引きは2024年以前のみ。packbackup.php の cleanup が $year=今年-1 未満を対象とするため)。 在庫表の表示クエリも毎回このテーブルを参照するため、今後さらに重くなる。 ・減量案: 直近数ヶ月の日次+それ以前は各月1日のみ残す。 在庫表は「期首=当月1日」「期末=翌月1日」を参照するため、各月1日は必ず残すこと。 DELETE ではなく「必要分を新テーブルへ INSERT → RENAME TABLE」が安全かつ速い (MyISAM の DELETE は領域が解放されず、35GB の OPTIMIZE TABLE に数時間かかる)。 ・InnoDB 化案: 行ロックになりロック起因の停止は避けられるが、現状 innodb_file_per_table=OFF のため ibdata1 が膨張して二度と縮まない(戻すには全DBダンプ&再構築)。 さらにデータサイズが2〜3倍になる一方 innodb_buffer_pool_size は 256MB しかなく、 在庫表はむしろ遅くなる公算が大きい。実施するなら減量が先。 2026-08-05 ellio/shearspec: PDF一括取込を実装(1ページ=1枚の寸法指定書) 背景: 寸法指定書のPDFは複数ページ=複数枚が1ファイルにまとまっているため、一括取込が必要。 サーバー調査(重要): zsystem.jp は **Mac OS X Server 10.6.8(Snow Leopard, 2011)/ PHP 5.3.26**。 Homebrew 非対応・pdftoppm/gs/ImageMagick いずれも無く、**poppler等の導入は非現実的** (ソースビルドは依存が深く、131日連続稼働の本番機でのリスクが高い)。 → 標準搭載の **Python 2.6 + Quartz(PyObjC)** でPDFをページ単位にPNG化できることを実機検証。 8ページ 6秒(150dpi) / 出力1754x1239px / 朱印の色・手書き数値まで判読可。**追加インストール不要**。 制限(運用取り決め): 1回 100ページ以下 かつ 32MB未満。 ※32MBは **post_max_size** の値。超えると $_FILES が空になり原因不明の失敗になるため、 ブラウザ側でも送信前にサイズを見て止める。upload_max_filesize は100Mだが post_max_size が効く。 実装: - pdfsplit.py: Quartzでページ分割(追加インストール不要) - bulkjob.php(CLI): ①分割 → ②1ページ=1レコード作成しimages//p-1.png へ移動 → ③順にAI読み取り。 進捗は images/_batch//state.json に書き、画面はポーリング。 ②完了時点でレコードは一覧に出るので、③を待たずに確認を始められる。 - ajax.php: mode=bulkStart / bulkStatus を追加 - import.php: 取込画面(進捗バー・サムネイル付き結果一覧)。index.php に導線追加 - ocrlib.php: ocrApplyToSpec() を追加。一括では人が1件ずつ反映できないため **空の項目にだけ**読取値を書き込む(人が入力済みの値は絶対に触らない) 所要の目安: 100ページなら 分割75秒+AI読み取り約1時間(Mac miniは1件ずつ処理のため順次)。 2026-08-05 ellio/shearspec: 一括取込の進捗を一覧からも追えるようにした 現象/指摘: 一括取込の画面を離れて一覧に戻ると、以後その取込の進行状況が分からない (取込画面を開き直しても初期状態になる)。AI読み取りは1件20〜90秒かかり、 100件なら1時間以上残るため、一覧側で「まだ処理待ちか」が見えないと運用にならない。 対応: - bulkjob.php: レコード作成の時点で ocrSetJob(...,'queued') を立てる(従来は処理開始まで印が無かった) - ajax.php listSpec: shearspec_field の __ocr_job__ を LEFT JOIN し ocr_status を返す (JOINに伴い WHERE 句の列名を s. 付きに修正) - index.php: 「AI読取」列を追加(待ち/処理中/済/失敗)。失敗はカーソルで理由を表示。 未処理が残っている間は上部に帯を出し、10秒ごとに自動再読み込みする - import.php: バッチIDを localStorage に保存し、画面に戻ったとき進行中の取込へ再接続する。 完了・失敗・状態消失時は保存を消して初期状態へ戻す デプロイ(2026-08-05): scp ellio/shearspec/{bulkjob.php,ajax.php,index.php,import.php}。 2026-08-05 ellio/shearspec: 一括取込の復旧(トンネル断)と、一覧の自動更新・状態バッジの見直し 【障害】8/5 13:55 のPDF一括取込(8ページ)でAI読み取りが8件とも失敗した。 原因は Mac mini への SSHリバーストンネル(zs:127.0.0.1:11500)の切断。 ・1段階目: ラッパー(php -S)が異常状態になり、PHPの起動時警告 「Unable to create temporary file / POST data can't be buffered」がHTTP 200の本文として返った。 13:55:45 の接続を受けたまま応答せず、10分後に launchd(KeepAlive) が再起動していた。 ・2段階目: 再実行すると HTTP 0(couldn't connect)。zsystem.jp 側に 11500 の listener が無く、 Mac mini の tunnel.log は Permission denied の繰り返し=autossh が復帰できていなかった。 対処: launchctl kickstart -k gui/$(id -u)/com.menou.ollama-ocr.tunnel でトンネル再確立。 サーバーから /health の応答を確認後、8件を ocrjob.php で再実行し全件成功(1件47〜56秒)。 ※このテスト取込分(id 38〜45)はユーザー指示により削除済み(レコード・shearspec_field・images/38〜45・images/_batch)。 ※未対応(次回検討): トンネルの死活監視、一括取込のリトライと連続失敗時の中断。 【一覧の自動更新】従来は未処理がある間、10秒ごとに mode=list(全列・最大500行・LEFT JOIN)を 取り直していた。100件取込では1時間その問い合わせが続くため、状況だけ返す軽いモードに分離した。 ・ajax.php: mode=ocrStates を追加(__ocr_job__ の {id, 状態} のみ返す) ・index.php: 変化した行のAI読取セルだけ差し替え、一覧全体の取り直しは 「どれかが done になった」「検索していない状態で知らないIDが増えた」ときだけ行う ・失敗だけが残っている状態でも30秒間隔で更新を続ける(従来は停止していたため、 再読み取りしても画面が古いままだった) 【状態バッジの整理】意味とラベルがずれていたので改めた。 ・「画像」→「未数値化」: fig_mode='image' は「紙がある」ではなく 「切断図が原本画像のままで数値(セグメント)になっていない」=シャーリング計画に使えない状態を指す。 紙の有無は scan_pagecount が持つ別情報。 ・「未確認」: 条件を intake_type=1 かつ verified=0 に限定(AI読み取り由来のみ)。手入力分には出さない。 ・「原本なし」を新設: scan_pagecount=0。基本は全件あるはずなので警告として出す。 【保存時の退行を修正】ajax.php saveSpec ・flagFields() から intake_type / verified を除外。画面から送っていないため 「未送信=0」の扱いで、保存のたびに intake_type が 1→0(AI読取の履歴が消える)になっていた。 ・numFields() から source_spec_id を除外し、送られてきたときだけ更新。 従来は保存のたびに転用元の紐付けが NULL に消えていた。 ・保存=人が内容を確定した、と扱い verified=1 / verified_by / verified_at を記録。 これで「未確認」が保存で外れる(従来は外す手段が無かった)。 担当者名は cookie の personname から取得(ajax.php は session_start() を通っていないため)。 AI読み取りをやり直すと ocrStoreFields() が verified=0 に戻すので、読み直した分は再び未確認になる。 【その他】ocrjob.php の未コミット分を併せて記録: DBホストを localhost→zsystem.jp(ellioユーザーは UNIXソケット経由の権限が無くAccess denied)、CLIで date.timezone 警告が出るため Asia/Tokyo を明示、 ジョブ状態の区切りを : から | へ(モデル名 qwen2.5vl:7b に : を含むため)。 index.php 側は旧 done:/error: 形式も読めるようにしてある。 デプロイ(2026-08-05): scp ellio/shearspec/{ajax.php,index.php,edit.php,shearspec.css}。 ※ocrjob.php は 7/31 時点で転送済み(サーバーと同一md5)。CSS/JSのバージョンを ?v=20260805a に更新。 2026-08-05 ellio/shearspec: 編集画面の原本スキャンをタブ化、一括取込画面の操作制御 【原本スキャン】小さいディスプレイでは原本スキャンが画面外に出てしまい、見るために縦スクロールすると 左の入力欄まで一緒に流れて上の項目が入力できなくなる、という指摘への対応。 ・edit.php: 右カラム下部の独立カード(srcCard)を廃止し、図と同じタブ列に 「原本スキャン」タブを追加(帳票 / 切断図のみ / 原本スキャン)。タブ名に枚数を出す(無ければ「なし」) ・shearspec.js: switchTab() を3タブ対応に。fig_mode='image' のときは中身の無い 「切断図のみ」タブだけを隠す(従来はタブ列ごと非表示にしていた)。 画像のみのレコードを開いたときと、原本をアップロードした直後は原本タブを自動選択 ・shearspec.css: .right .stage に max-height:calc(100vh - 96px)。 はみ出す分はカード内でスクロールさせ、ページ全体が伸びないようにした (タブ化だけでは帳票や図が長い場合に同じ問題が残るため)。印刷時は max-height:none で解除 【一括取込画面】アップロード中に「← 一覧」が押せてしまう/ファイル未選択でも「取込を開始」が 押せてしまう、という指摘への対応。保護するのは「開始の応答が返るまで」=送信中だけ。 応答後はサーバー側の別プロセス(bulkjob.php を exec で切り離し起動、CLIは max_execution_time=0) で進むため、画面を閉じても処理は続く。 ・import.php: 送信中は ← 一覧 / ファイル選択 / 開始ボタンを無効化し、beforeunload で確認ダイアログ。 開始ボタンは初期 disabled にし、32MB未満のファイルが選ばれているときだけ有効(syncStartBtn)。 従来は32MB以上でも押せて alert で止めていた。取込開始時に前回の結果表を消す ・shearspec.css: .btn:disabled のスタイルを追加(無効時の見た目が無かった) デプロイ(2026-08-06): scp ellio/shearspec/{import.php,shearspec.css}。 ※原本スキャンのタブ化分(edit.php / index.php / shearspec.js)は別セッションで先にコミット・転送済み (md5照合でサーバーと一致を確認)。本エントリはその作業記録を兼ねる。 CSS/JSのバージョンは ?v=20260805b。 2026-08-06 ellio/shearspec: AI読み取りをキュー化(多重起動しても失敗しないようにした) 背景: Mac mini 側のラッパーは php -S で同時1リクエストしか捌けず、Ollama も1枚ずつ推論する。 そのため取込を2つ動かす、または取込中に編集画面から読み取ると、後から来たものが CURLOPT_TIMEOUT=300 に達して失敗していた。「実行中なら弾く」ではなく順番待ちにする方針を採用。 設計: 直列化はバッチ単位ではなく **1枚単位**。cURLの区間だけをロックする。 バッチ単位だと100ページの取込中に単発の読み取りが1時間待たされるが、 1枚単位なら待ちは処理中の1枚(約1分)で済み、複数の取込も1枚ずつ交互に進む。 実装: - ocrlib.php: images/_ocr.lock を flock で確保(2秒間隔で再試行・上限30分 OCR_LOCK_WAIT)。 ocrRunImage($imgPath, $onStatus) に第2引数を追加し、待ちに入ったら 'waiting'、 順番が来たら 'running' をコールバック通知。ロックが開けないときは素通り(従来動作)。 異常終了してもOSがロックを解放するのでゴミは残らない。ローカルで直列化を実動作確認済み。 - bulkjob.php / ocrjob.php: 呼び出し前の ocrSetJob(...,'running') をやめ、 上記コールバックで waiting→running を書かせる - ajax.php ocrStart: 起動時は 'queued' のみ。実際の状態は ocrjob.php が書く - ajax.php ocrStatus: queued / waiting を正しく返すよう修正。 ※従来はこの2状態が done 扱いに落ちており、結果が空のまま「済」になり得た(不具合) - ajax.php bulkStart: 多重起動は禁止せず、実行中の他バッチ数を busy として返す - index.php: 「順番待ち」バッジを追加。帯の集計でも未処理として数える - shearspec.js: 編集画面のポーリングを「推論の経過」と「順番待ちの経過」に分離。 待ち時間は5分タイムアウトに算入しない(従来は他の取込を待つ間に打ち切られていた) - import.php: 他に実行中の取込があれば件数を警告表示 2026-08-07 ellio/dispatch: 現場打刻の車番入力で「同一車番に複数の実車」がある場合に選択ダイアログを出す 背景: carrierMaster には同じ car_no が別会社・別実車で登録されていることがある。 実測(2026-08-07)では車番 2637 が id12 岡田運輸 / 生井 満 / 4t平車 相模130あ2637 / 3,150kg id231 山本運送 / 大橋 / / 29,400kg の2台。従来 lookupCarrier() は ORDER BY sortno,id LIMIT 1 で先頭1件しか返さず、 現場が車番を打つと常に片方が採用され、もう一方は選ぶ手段が無かった。 実害(本作業で発見・修正済み): dispatch id976(2026-08-06 入荷・TSC・山本運送/大橋)は 打刻時刻 12:21:18 の時点で山本運送の master 行(id231)がまだ存在せず(作成は同日 14:06:01)、 岡田運輸の 3,150kg が dispatch.max_load に焼き付いていた。 会社名・ドライバーは後から再編集で直されたが max_load は saveTripEdit の対象外のため 3,150 のまま。 → 積載率 25,078/3,150 = 796.1% と表示されていた。 UPDATE dispatch SET max_load=29400 WHERE id=976 で修正(正しくは 85.3%)。 ※出荷便は dispatch.max_load が全132便とも 0 で表示時に carrierMaster を引き直すため、 この種の焼き付きは入荷でしか起きない(出荷側の積載量に誤りが無いことを確認済み)。 実装: - index.php lookupCarrier(): LIMIT 1 を撤去し active な全件を返す。 0件={hit:false} / 1件=従来形+count:1 / N件=従来形+{count,multi:true,candidates[]}。 複数時もトップレベルは先頭候補で埋め、旧クライアント互換を保つ - index.php createArrival(): carrier_id を受け付け、指定時はその master 行を採用。 これが無いと sortno 先頭が採られ、選ばれていない側の max_load を 「未登録なら補記」処理(559行〜)で書き換えてしまう。未指定時は従来動作 - dispatch.html: #carrierPickOverlay を新設(既存 arr-overlay/arr-pick 構造を流用)。 carrierBox に「選び直す」ボタンを追加 - dispatch.js: 補完カードの描画を applyCarrier() に切り出し、multi なら openCarrierPick() へ分岐。 arrCtx.carrierId / carrierCands を追加し createArrival へ carrier_id を送信。 候補が複数ある間は未選択だと「発送元選択へ」を非活性にし、×で閉じても先へ進ませない。 車番を打ち直したら前回の確定内容を破棄(別車番の会社/ドライバーが残らないように) - style.css: .carrier を3列グリッド化し .reselect と .carrier-row のスタイルを追加 ダイアログ表示項目: 会社名(大)+ドライバー氏名(必須2項目)+車格・ナンバー・最大積載(補助)。 1タップで確定(タブレット運用のため決定ボタンは置かない)。 対象は入荷 Step1 のみ。再編集ダイアログ(tripEditOverlay)は会社名・ドライバーが自由入力で master 照合していないため今回は変更していない。 残課題: 手入力モードで登録した master 新規行に max_load が入らない(tripEdit 経由も同様)。 id976 の事故の真因はここにあるため、別途対策を検討する。 デプロイ(2026-08-07): scp ellio/dispatch/{index.php,dispatch.js,dispatch.html,style.css} 2026-08-07 ellio/dispatchmng: 入荷カードの積載判定を出荷の流用から分離し、警告だけを表示する方針へ 背景1(善悪の逆転): 納入先カード右の「手配 / 荷重」と充足・不足バッジは出荷用の判定だったが、 入荷カードにもそのまま流用されていた。 出荷 = これから積む。手配した車の積載量が荷重に足りているか(足りない=不足=警告) 入荷 = すでに積んで来ている。荷重が積載量を超えていたら過積載(法令違反) 向きが逆なので、入荷で過積載の便が緑の「充足」と表示されていた。 実例: 2026-08-07 山信物流/好本達也(車番111) 最大積載3,150kg に対し荷重3,242kg。 「充足」と出ていたが実態は過積載。修正後は赤字「過積載」。 背景2(警告だけ目立たせる): 07-01〜08-07 のカード123枚のうち97枚が「充足」で埋まっており、 その中から異常を探す形になっていた。正常時は無表示にして、対処が要るものだけ色付きで出す。 実装(renderDestCard()): - $isIn で判定を分岐。出荷側の判定・表記は従来どおり - 入荷の判定: 荷重>最大積載=「過積載」(critical/赤) / 最大積載0=「積載量未登録」(warn) / 荷重0=「荷重未入力」 / それ以外=無表示(good) - 語の変更: 入荷は「手配」→「最大積載」。「手配」はこれから車を割り当てる出荷側の語で 到着済みの車には合わないため - 出荷の「充足」も無表示化(「不足 Nkg」「未割当」は対処が要るので残す) 結果: 123枚中、色付きで出るのは 過積載1件・未割当1件 のみ(他は荷重未入力7件)。 ※便行(renderTripRow)の「積載率」は元から >=100% で赤(.high)になる実装で入荷でも正しい向きのため未変更。 デプロイ(2026-08-07): scp ellio/dispatchmng/index.php 2026-08-07 ellio/dispatch: 「これではない(別の会社)」導線の追加と、手入力時のマスタ登録バグ修正 背景: 同日の 2637 対応(選択ダイアログ)の続き。車番を打って他社のトラックがヒットした場合に 「それは違う」と言う手段が画面に無かった。唯一の逃げ道は不特定業者用の 「その他(会社名手入力)」ボタンで、ヒット結果を否定する導線には見えない。 さらにサーバー側に実害のあるバグがあった: 手入力モードでは carrier_id を送らないため車番だけでマスタを引き直し $cmHitId が立つ。 すると (a) add_to_master の INSERT が else if に落ちず新しい会社がマスタに登録されない (「配送マスタに追加する」のチェックが黙って無視される) (b) ヒットした他社行の max_load を現場入力値で上書きする。 2026-08-06 の 2637 はまさにこの状況で、現場は別会社として登録できず いったん岡田運輸で通してから tripEdit で事後修正していた(max_load は 3,150 のまま残った)。 実装: - dispatch.html: carrierBox の操作ボタンを .carrier-acts でまとめ、 「これではない(別の会社)」ボタンを追加(単一ヒット時も常時表示) - dispatch.js: enterManualCarrier() を追加。車番を保ったまま手入力モードへ入り 会社名・ドライバー・最大積載を入力させる。選択ダイアログ末尾にも 「一覧にない(別の会社)」行を追加し同じ関数へ - dispatch.js: 手入力モード時に manual_mode=1 を送信 - index.php createArrival(): manual_mode=1 なら車番によるマスタ引き直しを行わない($cmHitId を立てない)。 これで add_to_master の INSERT が正しく走り、他社行の max_load 上書きも起きない - index.php createArrival(): 同一 (company, car_no) の行が既にある場合は INSERT せず、 その行の max_load が未登録なら今回の入力で補完する(ヒット時と同じ扱いに揃えた) → 「手入力で登録した master 行に最大積載が入らない」という残課題もこれで解消 - style.css: .carrier-acts を追加、.reselect を中央揃え・行間1.35(
での2行表示に対応) ※本番DBを汚さないため、手入力モードでの実登録(dispatch行+carrierMaster行の実作成)は未実施。 構文チェックと画面表示までの確認に留めている。実機で1件通して確認すること。 デプロイ(2026-08-07): scp ellio/dispatch/{index.php,dispatch.js,dispatch.html,style.css} 2026-08-07 ellio/dispatchmng: 配車管理記録のセル編集(管理者のみ・ダブルクリック)+ログインチェック追加 目的: 配車管理記録(21列のフラット表)の項目を、できる限り画面上で直せるようにする。 編集可否の切り分け(ユーザーと合意): 編集可(15列): 会社名/氏名/車番/E指示受付/F受付到着/G積込開始/H積込完了/I退場/ 状況/内容(納入先)/梱包数/重量kg/最大積載/天候/備考 編集不可(6列): 日付(グループ移動が起きるため禁止) / 滞在分・荷待分・荷役分・積載率(計算値) / 走行km(customertable 由来。1行直すと同一納入先の全便に波及するため画面では直せない) ※荷待分・荷役分は dispatch.wait_time/work_time が誰にも書かれておらず常に0。今回は現状のまま。 最大積載の方針: dispatch.max_load に書く(=この便の実績を直す)。carrierMaster は触らない。 結果としてその便は max_load>0 になり、以後 carrierMaster を直しても追従しなくなる(意図どおり)。 天候の方針: 実装を確認したところ、打刻時点の pill 選択値をその行だけに書く実装で、 「1日1回入力・その日の全レコードに一括反映」というスキーマのコメントが実態と食い違っていた。 実装が正しいのでコメント側を修正 → alter_weather_comment.sql(データ型・値は不変)。dispatch.sql も追従。 実装: - index.php dispForm(): [ISADMIN] を追加(CheckPermission('300')) - index.php readRecordJson(): 返却に id を追加(更新APIに必須) - index.php updateDispatch(): 冒頭に CheckPermission('300') ガード。 $allowed に max_load / weather を追加。package_count/weight/max_load は INT キャスト+カンマ除去 (文字列のまま UPDATE すると MySQL 側で黙って 0 に丸められるため)。 値の妥当性検証は入れない=内容の責任は編集者にある運用 - dispatchmng.js: IS_ADMIN を追加。rcell() で編集可セルのみ class="editable" と data-* を付与 (非管理者には data 属性ごと出さない)。bindRecordEdit() は tbody への委譲リスナ1つ。 dblclick で input 化 → Enter/blur で保存・Esc で取消。保存後は loadRecord() で引き直し (滞在分・積載率など算出値の整合を保つため) - dispatchmng.html / style.css: modal-head に「ダブルクリックで編集が可能です」バッジ (管理者のみ表示・地色 --ink-3 に白抜き)。編集可セルの hover 表示と保存成功フラッシュ ログインチェック: - dispatchmng/index.php に LoginCheck("100") を追加。switch より前に置き AJAX の各 mode も保護する (画面だけ塞いでも mode=updateDispatch を直接叩けてしまうため) - ★dispatch/(現場打刻)には掛けない判断。LoginCheck() はリファラが zsystem.jp/localhost の ときしかクッキーでのログインを認めず、さらにクッキー寿命が8時間しかない。 現場タブレットはブックマーク/ホーム画面から直接開くため朝イチの打刻が止まる。 将来「認証漏れ」と誤解されないよう index.php 冒頭にコメントを残した 動作確認: 管理者(9005)で IS_ADMIN=1・ヒント表示、非管理者で IS_ADMIN=0・editable属性なし、 非管理者の直POST → 「ERR: 権限がありません」、許可外フィールド → field not allowed、 数値に "3,150" → 3150 で保存。 ブラウザでの dblclick 実操作(input化・Enter/Esc・保存後の再読込)もデプロイ前にユーザーが確認済み。 ※作業中の API 疎通確認で本番 dispatch id977 に実書き込みをしてしまった。 max_load は 0 に戻したが、weather に書いた「晴」は元値が復元できていない(同日の他行から推定して晴の可能性が高い)。 ローカル確認: LoginCheck のリファラ判定により 127.0.0.1 では入れない。 http://localhost:8888/ellio/menu/ からリンクで遷移すること。 デプロイ(2026-08-07): scp ellio/dispatchmng/{index.php,dispatchmng.js,dispatchmng.html,style.css} + ellio/dispatch/index.php、および alter_weather_comment.sql の実行