概要

項目内容
案件 IDMAS-232
案件名サイドバー UI の SPA 化(Vite + React + TypeScript)
カテゴリUX / 基盤
優先度P2 ★★(MAS-057 Phase 3 / MAS-056 Phase 1 の共通基盤 + Jr 初期戦力化 + サイドバー応答 5-20 倍改善)
前提案件MAS-217(サイドバー統合 + 視認性改善・仕様書完了 2026-04-20)/ MAS-224(GitHub Actions CI パイプライン・未着手)/ MAS-230(Jr 採用要件・未着手)
後続連携 / 共通基盤提供先MAS-057 Phase 3(ビジュアルコックピット・UI はクライアントサイド計算前提)/ MAS-056 Phase 1(Chat UI + マルチシナリオ)/ F-57b 候補(左右分割ワークスペース UI・2026-04-24 会話合意・将来案件として切出)
吸収・再定義対象該当なし(MAS-217 の CSS 資産は継承、SPA 版で React コンポーネント化)
採用技術スタック(v1.2 PoC Stage 1 で確定・PR #361)Vite 5^5.4.10)/ React 18^18.3.1react-dom 同バージョン)/ TypeScript 5^5.6.0strict: true + noUnusedLocals + noUnusedParameters)/ vite-plugin-singlefile 2^2.0.3・全アセットインライン化)/ @vitejs/plugin-react 4^4.3.4)/ ビルド設定: assetsInlineLimit: 100_000_000 + cssCodeSplit: false + inlineDynamicImports: true(外部参照完全排除)
実機検証メトリクス(v1.2 PoC Stage 1)bundle 145KB / gzip 47KB(目標 500KB 以内・余裕あり)/ 外部 CDN 参照 0 件 / インライン <script> 1 個(全 JS/CSS インライン化)/ CSP 違反 0 件(Chrome DevTools Console 確認済)/ google.script.run 疎通 Promise wrapper 経由で成功 / useState による再描画動作
新規追加ファイル(Phase 1 = PoC Stage 1 確立済 + Stage 2 以降)フロント(v1.2 確定): webapp_client/(repo root 直下)package.json / tsconfig.json / vite.config.ts / sidebar_spa_shell.html / src/main.tsx / src/App.tsx / src/gas-bridge.ts / README.md)。tools/webapp-client/templates/sidebar_spa/ は不採用(理由: templates/ 配下は GAS に push される場所なのでソース混入リスク、tools/ は本プロジェクトに既存慣習なし)/ ビルド成果物: templates/sidebar_spa_shell.html(Vite 単一 HTML ビルド・git commit 必須・GAS へ push)/ GAS 側(PoC Stage 1 配置): 100_config/101_sys_config.jsopenSpaSidebarPoc() / spaPocGetEnvInfo() / spaPocEcho(msg) を実装(v1.1 で指定した 300_ui/302_spa_bridge.js 分離は Stage 2 本格エントリ(例: openHitlSidebarSpa)着手時に実施予定・PoC 段階は 3 関数のみのため分離コスト > 利益と判断)/ 型定義: docs/spec/sidebar_api.d.ts(Stage 2 で新設予定・JSON-safe 型のみ許容・Date 型禁止ルール明記
新規シートなし
新規 03_sys_params キーN56_STATE_MGMT_LIBZUSTAND / JOTAI・デフォルト ZUSTAND・PoC 後に確定)、N56_ENABLE_SPAtrue / false・段階移行用フィーチャーフラグ・デフォルト false・各機能別に有効化)
段階移行順序(v1.0 暫定)(a) MAS-147 v2 HitL サイドバー(Phase 1 PoC 本命・先行移行) → (b) MAS-175 OCR 失敗フォーム → (c) MAS-174 小口現金入力 → (d) MAS-217 既存 operations_sidebar.html 統合版。着手順序は PoC 結果で v1.1 に反映

MAS-217(サイドバー統合 + 視認性改善・仕様書完了)の続編。vanilla JS + inline CSS + google.script.run ラウンドトリップで構築されている既存サイドバー 6 ファイルを Vite + React + TypeScript の SPA に段階移行し、初回 1 往復のマスタロード後はクライアント完結で体感 5-20 倍の応答改善を実現する。

本案件は同時に以下を担う:

  • MAS-057 Phase 3 / MAS-056 Phase 1 の共通基盤(両案件の UI は Vite + React 前提・MAS-057 spec v1.7 参照)
  • 将来の左右分割ワークスペース UI(F-57b 候補)の足場(右側=操作パネル / 左側=HitL・シミュレーション結果・2026-04-24 会話で合意)
  • Jr エンジニアの独立作業領域(会計ロジック不要・React/TS 経験だけで参加可能・MAS-230 採用要件と連動)

PoC 前提の柔軟性(v1.0 方針): 本案件は仕様書 v1.0 で技術選定(状態管理ライブラリ・ディレクトリ配置・段階移行順)を暫定推奨に留め、main 側の PoC(MAS-147 v2 HitL サイドバーを 1 機能だけ先行 SPA 化)の結果を v1.1 で反映する前提で書かれている。

目的

  • サイドバー操作の体感 5-20 倍改善: MAS-217 TODO 行に示された目標。google.script.run のラウンドトリップ(500ms-2s)を初回 1 往復に削減し、以降の検索・フィルタ・画面遷移はクライアント完結(< 16ms)とする。
  • MAS-057 Phase 3 の前提基盤提供: MAS-057 Phase 3 は「GAS 側と同一の .js ファイルをクライアントでも直接読み込み、action=F57_BOOTSTRAP でマスタ一括配信する」設計(MAS-057 spec v1.7 注意事項 #14)。本案件で Vite + React + GAS ブリッジの基盤を確立することで、MAS-057 Phase 3 が即着手可能となる。
  • MAS-056 Phase 1 の Chat + マルチシナリオ UI の前提基盤: MAS-056 Phase 1 は vite-plugin-singlefile + クライアント側ポーリング + クライアント主導型 Tool Use オーケストレーション前提(MAS-332 Deep Research 結果・MAS-056 spec)。本案件完成で MAS-056 Phase 1 の技術的リスクが解消。
  • 将来の左右分割ワークスペース UI(F-57b 候補)の足場: 2026-04-24 会話での当初要望(右=操作 / 左=HitL・シミュレーション結果の統合 UI)は SPA 基盤 + React Router hash ルーティング前提。本案件で基盤確立後、F-57b を別案件として切出して実装する方針で合意済。
  • Jr エンジニアの初期戦力化案件: GAS / 会計ロジックの習熟なしでも、React/TypeScript の経験だけで独立して実装・レビューが可能。MAS-230 採用要件(React/TS 経験優遇)の実証案件として位置付ける。
  • google.script.run 呼び出し削減による Google Apps Script クォータ緩和: 1 日当たりの URL Fetch / Script runtime のクォータ消費を削減し、商用化時のマルチテナント運用に備える。

現在のコード

既存サイドバー 6 ファイルの現状(2026-04-25 時点)

ファイル行数用途本案件 SPA 化優先度
templates/operations_sidebar.html206メイン操作パネル(9 セクション・41 ボタン・MAS-217 視認性改善済)(d) Phase 1 後半
templates/invoice_hitl_sidebar.html464MAS-147 v2 HitL レビュー・最も複雑・操作 UX が分かりやすい(a) Phase 1 PoC 本命
templates/what_if_sidebar.html507MAS-011 What-if シミュレーション(ドライバーフォーム)Phase 2
templates/hiring_tco_sidebar.html210MAS-048 採用 TCO シミュレーターPhase 2
templates/wage_tax_credit_sidebar.html193MAS-049 賃上げ促進税制シミュレーターPhase 2
templates/sim_headcount_dialog.htmlMAS-012 人員計画(ダイアログ型)Phase 2 以降

全て共通の構造: <?= ?> GAS テンプレート記法 + vanilla JS + inline <style> + google.script.run.withSuccessHandler/withFailureHandler を直接ボタン onclick から呼ぶ形。

GAS 側エントリポイント(100_config/101_sys_config.js

openOperationsSidebar()       → line 356 → templates/operations_sidebar
openWhatIfSidebar()           → line 371 → templates/what_if_sidebar
openInvoiceHitlSidebar()      → line 384 → templates/invoice_hitl_sidebar
openHiringTcoSidebar()        → (別行)    → templates/hiring_tco_sidebar
openWageTaxCreditSidebar()    → (別行)    → templates/wage_tax_credit_sidebar
openHeadcountSimulation()     → (別行)    → templates/sim_headcount_dialog
doGet(e)                      → line 400 → ?view=ops|hitl で切替(Web アプリ)

全て共通パターン: HtmlService.createTemplateFromFile(name)template.envName = ...template.evaluate().setTitle(...).setWidth(...)SpreadsheetApp.getUi().showSidebar(html)

MAS-217 の既存 CSS 資産(継承対象)

MAS-217 で確定する data-cat="rpa" / "match" / "regist" / "mart" / "maintenance" / "devops" / "migration" / "dev" / "test" の 9 セクション色分け CSS + .btn.read / .write / .destroy のボタンバリアントは、本案件で React コンポーネント化(<Section category="rpa"> / <ActionButton kind="write">)して継承する。

package.json の現状技術スタック

{
  "type": "commonjs",
  "dependencies": { "@google/generative-ai": "^0.24.1", "marked": "^15.0.12" },
  "devDependencies": { "typescript": "^5.9.3", "live-server": "^1.2.2", "nodemon": "^3.1.14", "dotenv": "^17.4.2" }
}

Vite / React / @vitejs/plugin-react / vite-plugin-singlefile / Zustand / Jotai / @types/react は未導入。Phase 1 で追加する(ルート package.json に追加するか、サブパッケージ tools/webapp-client/package.json を新設するかは §人間検討事項参照)。

GAS HtmlService の CSP 制約(実装前調査済)

  • 外部 CDN 読み込み禁止: <script src="https://cdn.jsdelivr.net/..."><link href="https://fonts.googleapis.com/..."> 等は CSP でブロックされる → vite-plugin-singlefile で全アセット(JS / CSS / フォント)をインライン化必須
  • eval 禁止: React 18+ の development mode は eval を使用するため、production mode でのみビルドする
  • window.addEventListener('message') は使用可能: 親ウィンドウ(スプレッドシート)との通信は可
  • iframe sandbox: 親スプレッドシートの DOM / Cookie / localStorage には直接アクセス不可
  • React Router は hash ベース(#/...)のみ動作: pushState / BrowserRouter は iframe sandbox 制約でブロック・HashRouter 採用必須

MAS-332 Deep Research の確定方針(2026-04-24 完了)

docs/_internal/research_prompts/RQ-034_conversational_scenario_ui_result.md で調査済:

  • Chat UI 基盤 = Vite + React (TS) + vite-plugin-singlefile で単一 HTML ビルド → HtmlService.createHtmlOutputFromFile or doGet 返却
  • 通信 = google.script.run + クライアント側ポーリング(GAS 側の重い処理は時間差トリガー起動に逃がし、クライアントから数秒おきに checkStatus(id) を問い合わせ、30/60 秒タイムアウトを完全回避)
  • Tool Use オーケストレーション = クライアント主導型(「AI → React → GAS → React → AI」連鎖)

本案件は上記調査結果を踏まえた設計とする。

修正方針

Step 1 — ツールチェーン(v1.2: PoC Stage 1 で確立済・PR #361

ゴール: cd webapp_client && npm install && npm run buildtemplates/sidebar_spa_shell.html が生成されること。本仕様書 Stage 2 以降の手順は下記の確定構成を前提に書かれている

確定構成(PR #361 で実地検証済・実装実態)

webapp_client/package.json:

{
  "name": "webapp-client",
  "private": true,
  "version": "0.0.1",
  "description": "N-56 PoC: Vite + React + vite-plugin-singlefile で GAS HtmlService 用の単一 HTML を生成するクライアント側バンドル。",
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.0",
    "vite": "^5.4.10",
    "vite-plugin-singlefile": "^2.0.3"
  }
}

webapp_client/vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import { resolve } from 'node:path';

/**
 * N-56 PoC: GAS HtmlService CSP 制約下で動作する単一 HTML を生成する。
 * すべての JS/CSS をインライン化し、外部リソースへの参照を排除する。
 * ビルド成果物は repo root の templates/sidebar_spa_shell.html に出力し、
 * GAS 側 HtmlService.createHtmlOutputFromFile('templates/sidebar_spa_shell') で読み込む。
 */
export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: {
    outDir: resolve(__dirname, '../templates'),
    emptyOutDir: false, // templates/ には他の .html があるため全消しは厳禁
    rollupOptions: {
      input: resolve(__dirname, 'sidebar_spa_shell.html'),
      output: {
        inlineDynamicImports: true, // viteSingleFile が結合するため強制
      },
    },
    assetsInlineLimit: 100_000_000, // GAS HtmlService は外部リソース禁止
    cssCodeSplit: false,
  },
});

webapp_client/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "types": ["vite/client"]
  },
  "include": ["src"]
}

Stage 2 以降で追加が必要な依存関係(未導入):

  • 状態管理: zustand(暫定推奨・useState で足りないケースで導入判断・§人間検討事項 #1)
  • ルーティング: react-router-domHashRouter(複数 view 切替時に導入・§人間検討事項参照)
  • CSS: Tailwind / CSS Modules 等(Stage 2 で本格移植時に選定・§人間検討事項 #6)

Step 2 — ディレクトリ構成(v1.2: webapp_client/ 採用確定・PR #361

採用構成(実地確定・PoC Stage 1):

<repo-root>/
├─ webapp_client/                      # 新設・clasp push 対象外(.claspignore 経由)
│   ├─ package.json                    # 独立した依存管理(ルートに汚染しない)
│   ├─ tsconfig.json                   # strict: true
│   ├─ vite.config.ts                  # outDir=../templates/
│   ├─ sidebar_spa_shell.html          # Vite エントリ HTML(ビルド入力)
│   ├─ src/
│   │   ├─ main.tsx                    # React root マウント
│   │   ├─ App.tsx                     # UI コンポーネント(Stage 2 以降で panels/ に分割予定)
│   │   └─ gas-bridge.ts               # google.script.run Promise wrapper
│   └─ README.md                       # 使用手順
└─ templates/
    └─ sidebar_spa_shell.html          # Vite ビルド成果物(commit 必須・GAS に push)

採用理由(不採用案との比較・PR #361 で確定):

候補採用判断理由
webapp_client/(repo root 直下)✅ 採用templates/ 配下ではないのでソース混入リスクなし・tools/ 既存慣習に縛られない・clasp 側の .claspignore で明示的に除外済
tools/webapp-client/❌ 不採用本プロジェクトに tools/ 慣習が既存なく、命名独自感が強い
templates/sidebar_spa/❌ 不採用templates/ は GAS に push される場所なのでソースが意図せず混入する恐れ + .claspignore 設計複雑化

Step 3 — 型安全 API bridge(v1.2: gas-bridge.ts 実装確立済・PR #361

ゴール: google.script.run を TypeScript typed な Promise API としてラップ。

確定実装(PR #361・webapp_client/src/gas-bridge.ts

/**
 * N-56 PoC: google.script.run の typed Promise wrapper。
 * 呼び出し毎に `withSuccessHandler` / `withFailureHandler` を登録する反復を隠蔽する。
 */

interface GasScriptRun {
  withSuccessHandler: (fn: (result: unknown) => void) => GasScriptRun;
  withFailureHandler: (fn: (error: Error) => void) => GasScriptRun;
  [fnName: string]: unknown;
}

interface GasGlobal {
  script?: { run: GasScriptRun };
}

/**
 * GAS 関数を Promise として呼び出す。
 * 例: await gasRun<string>('getEnvName');
 */
export function gasRun<T = unknown>(fnName: string, ...args: unknown[]): Promise<T> {
  const gas = (globalThis as unknown as { google?: GasGlobal }).google;
  if (!gas?.script?.run) {
    return Promise.reject(
      new Error('google.script.run が利用できません (GAS HtmlService 環境外)')
    );
  }
  return new Promise<T>((resolve, reject) => {
    const chain = gas.script!.run
      .withSuccessHandler((result) => resolve(result as T))
      .withFailureHandler((error) => reject(error));
    const fn = (chain as unknown as Record<string, (...args: unknown[]) => void>)[fnName];
    if (typeof fn !== 'function') {
      reject(new Error(`GAS 関数 "${fnName}" が定義されていません`));
      return;
    }
    fn(...args);
  });
}

使用例(PR #361 の App.tsx より)

import { gasRun } from './gas-bridge';

const info = await gasRun<{ envName: string; timestamp: string; spreadsheetId: string }>('spaPocGetEnvInfo');
const echoed = await gasRun<string>('spaPocEcho', 'hello');

Stage 2 以降の拡張方針

  • docs/spec/sidebar_api.d.ts SSoT の新設(Stage 2 で着手): SidebarApi interface に GAS サーバー関数シグネチャ(spaPocGetEnvInfogetHitlPdfs / approveHitl 等)を集約。gasRun<K extends keyof SidebarApi>() のように型付けを強化。

  • Date 型禁止規約(v1.1 確立・v1.3 で TypeScript 型システムによる強制に昇華): sidebar_api.d.ts 先頭にコメント規約だけでなく、以下のユーティリティ型を定義して SidebarApi の戻り値を物理的にラップする:

    /**
     * google.script.run 経由で Date 型を使うと自動文字列化されるため、
     * 戻り値に Date が含まれると React 側で .getFullYear() 等がランタイムエラーになる。
     * この型でレスポンスをラップすれば、型定義時点で Date 使用をコンパイル時に弾ける。
     */
    export type JSONSafe<T> = T extends Date
      ? never  // Date 型は使用禁止
      : T extends Function
      ? never  // Function も不可
      : T extends object
      ? { [K in keyof T]: JSONSafe<T[K]> }  // 再帰的に検査
      : T;
    
    export interface SidebarApi {
      getInitialStateForSpa(view: string): JSONSafe<InitialStateForSpa>;
      runInvoiceBatchPublic(): JSONSafe<string>;
      // ... 段階追加
    }
    

    Jr が誤って Date を含めても、JSONSafe<T>never に解決されて tsc が型エラーを返すため、CI 通過前に必ず検知される。

  • 名前空間分離(Stage 2 で検討): gasRun() 直呼びから、gasBridge.hitl.approve(id) のような domain-scoped API に移行するか否か。

Step 4 — 段階移行(Stage 1-5)と Go/No-Go 基準(v1.2 で PoC 結果を踏まえて精緻化)

移行段階(v1.2 確定):

段階対象状態Go/No-Go 基準
Stage 1(PoC)Vite+React+singlefile の GAS HtmlService 疎通検証完了 2026-04-24 (PR #361)bundle 145KB/gzip 47KB・CSP 違反 0 件・google.script.run 疎通 OK・useState 動作確認・4/4 検証項目クリア
Stage 2MAS-147 v2 HitL サイドバー(invoice_hitl_sidebar.html 464 行)を React 移植未着手(main 側 feat/n-56-stage2-i03v2-hitl-migration で予定)移植後の UX が従来版と同等以上・bundle 500KB 未満維持・承認/スキップ動作に回帰なし
Stage 3MAS-175 OCR 失敗フォーム(案件未実装)ブロックMAS-175 の GAS 実装完了後に着手(MAS-175 自体が未実装のため依存待ち)
Stage 4MAS-174 小口現金(案件未実装)ブロックMAS-174 の GAS 実装完了後に着手。Jr 独立担当領域として最適
Stage 5MAS-217 既存 operations_sidebar.html 統合版(9 セクション 41 ボタン)未着手他機能の SPA 移植が揃ってから(Stage 2-4 完了後・並行稼働期間を最大化)

: MAS-048 / MAS-049 / MAS-011 / MAS-012 等の個別シミュレーター系サイドバーは Stage 5 以降のオプション案件として検討。

Step 5 — GAS 側配信エンドポイント(v1.2: PoC Stage 1 実装確立済・Stage 2 以降の展開方針

Stage 1 実装状態(PR #361): 100_config/101_sys_config.js:423-451 に PoC 用 3 関数を配置:

  • openSpaSidebarPoc() — サイドバー起動(PoC 用)
  • spaPocGetEnvInfo() — 環境情報返却(初回ロード時)
  • spaPocEcho(msg) — エコー(ボタンクリック時)

v1.1 で指定した 300_ui/302_spa_bridge.js への UI 責務分離は Stage 2 本格エントリ着手時に実施(例: openHitlSidebarSpa / getInitialStateForSpa(view) / handleSpaDoGet_(e) を本命として追加する段階で配置先を決定)。PoC 段階の 3 関数のみでは分離コスト > 利益と main 側が判断し、101_sys_config.js に暫定配置(本仕様書 v1.1 指定より優先)。

Stage 2 以降の配置方針(本仕様書推奨):

  1. UI 責務分離(v1.1 継承・Stage 2 着手時に実施):

    • 300_ui/302_spa_bridge.js(新設)に SPA UI エンドポイント集約
    • 101_sys_config.js の PoC 用 3 関数(spaPoc*)は Stage 2 完了後に削除
    • 101_sys_config.jsdoGet(e)view.endsWith('_spa') 検知で handleSpaDoGet_(e) へ委譲のみ
    • 配置転換対象: openSpaSidebarPocopenHitlSidebarSpa / spaPocGetEnvInfogetInitialStateForSpa / spaPocEcho → 削除
  2. 配置方針(v1.1 Major 反映・Modular Monolith 層分離):

    • UI エンドポイント関数群(open*SidebarSpa() / getInitialStateForSpa() / handleSpaDoGet_())は **300_ui/302_spa_bridge.js(新設)**に配置する。ただし getInitialStateForSpagoogle.script.run から直接呼ばれるため末尾 _ を付けない(GAS 仕様で末尾 _ 関数は private 扱いとなりクライアント不可・failure_patterns #28)
    • 100_config/101_sys_config.js は設定層・DDL 定義に専念し、UI ロジックを持たない(Stage 2 移送完了後)
    • 既存 101_sys_config.jsopenOperationsSidebar() 等は従来版のため変更なし(並行稼働中は現状維持し、Stage 5 時点で 302_spa_bridge.js 側へ移植を検討)
  3. 300_ui/302_spa_bridge.js(新設)の実装方針(v1.3 Critical 反映・Race Condition 回避):

    v1.1 で指定した PropertiesService.getUserProperties() 経由の view 伝達(_setSpaView_ / _consumeSpaView_)は、ユーザーが複数タブで別々のサイドバーを立て続けに開いた際にプロパティが上書きされて誤った画面が表示される Race Condition を引き起こすため撤回。代わりに HtmlOutput.append()ステートレスに JS 変数を注入する方式に統一する(v1.3 で確立)。

    /**
     * SPA 初回ロード時の一括取得関数 (N-56)。
     * React マウント直後にクライアントから呼び、マスタ + 設定を 1 回で返す。
     * view はクライアント側 window.SPA_INITIAL_VIEW から取得済のため引数で受ける。
     */
    function getInitialStateForSpa(view) {
      var isSpaEnabled = Constants.getParam('N56_ENABLE_SPA', 'false') === 'true';
      if (!isSpaEnabled && !Env.isDev()) {
        throw new Error('N56_ENABLE_SPA=false のため SPA 版は無効です');
      }
      return {
        envName: Env.name().toUpperCase(),
        isDev: Env.isDev(),
        hasTestRunner: (typeof runAllTests === 'function'),
        view: view,
        menuDefinition: Constants.MENU_DEFINITION,
        // view 別の追加データ(Date 型は禁止・ISO8601 string or epoch number で返却)
        hitlPdfs: view === 'hitl' ? _listInvoiceHitlPdfs_() : null,
        sysParams: _getPublicSysParamsForSpa_(),
      };
    }
    
    /**
     * SPA サイドバーを開く(Stage 2 I-03 HitL 版)。
     * v1.1 Critical 反映: evaluate() ではなく createHtmlOutputFromFile() を使う。
     * v1.3 Critical 反映: view 情報は HtmlOutput.append() で <script> タグとして注入し、
     * PropertiesService を介さずステートレスに伝達する(複数タブ対応)。
     */
    function openInvoiceHitlSidebarSpa() {
      var html = HtmlService.createHtmlOutputFromFile('templates/sidebar_spa_shell')
        .setTitle('🗂️ 請求書 HitL レビュー (SPA)')
        .setSandboxMode(HtmlService.SandboxMode.IFRAME);
      // ステートレスに初期 view を JS グローバル変数として注入
      html.append('<script>window.SPA_INITIAL_VIEW = "hitl";</script>');
      SpreadsheetApp.getUi().showSidebar(html);
    }
    
    /**
     * doGet(e) の SPA 分岐ハンドラ(Web App 用)。
     * doGet 本体(101_sys_config.js)から呼ばれ、view=*_spa を検知したら本関数に委譲する。
     * view は URL パラメータから取得し、HtmlOutput.append() で JS 変数として注入する。
     */
    function handleSpaDoGet_(e) {
      var view = ((e && e.parameter && e.parameter.view) || 'hitl_spa').replace('_spa', '');
      var html = HtmlService.createHtmlOutputFromFile('templates/sidebar_spa_shell')
        .setTitle('🚀 BizLP ERP (SPA)');
      // XSS 予防: view はアルファベット + アンダースコアのみ許可
      var safeView = String(view).replace(/[^a-z_]/g, '');
      html.append('<script>window.SPA_INITIAL_VIEW = "' + safeView + '";</script>');
      return html;
    }
    

    FOUC 対策(v1.3 Minor 反映): React 側 main.tsxwindow.SPA_INITIAL_VIEW をマウント前に読み取り、HashRouterinitialEntries に即座に設定することで、初期 URL / のチラつき(FOUC)を防ぐ。

    // webapp_client/src/main.tsx(Stage 2 で実装予定)
    const initialView = (window as any).SPA_INITIAL_VIEW || 'hitl';
    ReactDOM.createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <HashRouter initialEntries={[`/${initialView}`]}>
          <App />
        </HashRouter>
      </StrictMode>
    );
    
  4. 100_config/101_sys_config.jsdoGet(e) への分岐追加( 1 ブロック挿入・UI 実装は 302_spa_bridge.js に委譲):

    function doGet(e) {
      var view = (e && e.parameter && e.parameter.view) || 'hitl';
      if (view.endsWith('_spa')) {
        return handleSpaDoGet_(e);  // UI ロジックは 302_spa_bridge.js に委譲
      }
      // 既存の非 SPA 版ロジック(変更なし・並行稼働)
      // ... 既存
    }
    
  5. MENU_DEFINITION に 1-2 項目追加(既存並行稼働・登録は 101_sys_config.jsMENU_DEFINITION 本体):

    // 📋 サイドバー: 🔧 開発・設定 カテゴリに追加(funcName は 302_spa_bridge.js の関数を参照)
    { label: '🧪 HitL レビュー (SPA)', funcName: 'openInvoiceHitlSidebarSpa', description: 'Vite+React SPA 版(N-56 PoC・実験的)' },
    

設計原則(v1.1 で確立・v1.3 で Race Condition 対策に更新・今後の全 SPA サイドバーの標準):

  • テンプレート評価(<?= ?>)は一切行わない: createHtmlOutputFromFile() で静的ファイルをそのまま配信(v1.1)
  • view / 初期状態の伝達は HtmlOutput.append('<script>window.VAR=...;</script>') でステートレス注入: v1.1 で暫定指定した PropertiesService 経由は複数タブ race condition のため v1.3 で撤回。append() で HTML 末尾に <script> タグを追加する方式が GAS 公式推奨パターンで、並列起動時も各 iframe が独立した window を持つため競合なし。XSS 予防のため変数値は String(x).replace(/[^a-z_0-9-]/g, '') 等で必ずサニタイズする
  • UI 責務は 300_ui/ 層に集約: 100_config/ / 200_data/ / 400_domain/ には UI エンドポイントを置かない(v1.1)

影響範囲

対象種別変更内容リスク
tools/webapp-client/ または templates/sidebar_spa/追加フロントソース一式(暫定推奨)ディレクトリ配置は PoC で確定
templates/sidebar_spa_shell.html追加Vite ビルド成果物(git 管理必須)commit 漏れで clasp push 時の事故リスク
100_config/101_sys_config.js変更getInitialStateForSpa() + openInvoiceHitlSidebarSpa() 新設 + MENU_DEFINITION に 1-2 項目追加 + doGet(e) 分岐追加既存関数は変更なし・並行稼働
既存サイドバー 6 ファイル変更なしPhase 1 では既存版をそのまま維持(並行稼働期間中)MAS-217 視認性改善は既存版にのみ反映(SPA 版は React コンポーネントで対応)
package.json(ルート)変更scriptsbuild:spa / dev:spa / type-check:spa 追加。依存追加の場所は §人間検討事項で確定(ルート追加 or サブパッケージ分離)サブパッケージ採用なら既存ビルドに影響なし
.claspignore変更tools/webapp-client/ or templates/sidebar_spa/ 配下の source を除外(ビルド成果物 templates/sidebar_spa_shell.html のみ push)除外漏れで GAS クォータ消費
scripts/pre-push-check.sh変更npm run build:spa を前置 + git status で成果物の未コミット検知ビルド失敗時の push ブロック
docs/spec/sidebar_api.d.ts追加GAS サーバー関数シグネチャ集約(SSoT)GAS 実装との手動同期が必要(MAS-224 CI で検出)
docs/_internal/changelog.md変更初版作成エントリ追加影響なし
docs/_internal/TODO_future.md変更MAS-232 行のステータスを「未着手」→「仕様書完了」に変更・完了日 2026-04-25影響なし
docs/_config.json変更nav に本 spec ファイルを登録影響なし
appsscript.json変更なしReact 導入で OAuth スコープ追加不要(CSP 内完結)failure_patterns #26 遵守

注意事項

本案件の実装時に注意すべき既知の落とし穴・設計制約。

  1. failure_patterns #18-#20(命名造語禁止): パッケージ名(vite-plugin-singlefile / @vitejs/plugin-react 等)は npm レジストリで実在を Read で裏取りしてから使用。関数名(getInitialStateForSpa / openInvoiceHitlSidebarSpa)は既存命名規則(動詞 + 目的語 + Spa サフィックス)と整合させる。末尾 _ は付けない(GAS 仕様で末尾 _ 関数は private 扱いとなり google.script.run から呼べない・failure_patterns #28)。
  2. failure_patterns #25(並列実装対称性): Phase 1 (a)-(d) の段階移行で open*SidebarSpa() 関数を追加する際、既存の open*Sidebar() と対称な形(引数なし・戻り値なし・HtmlService.createTemplateFromFile 使用)を保つ。
  3. failure_patterns #26(oauthScopes 部分宣言禁止): appsscript.json には一切変更を加えない。Vite ビルド / React は既存 CSP + OAuth スコープ内で完結する。
  4. GAS HtmlService CSP: 外部 CDN 禁止: <script src="https://cdn.jsdelivr.net/..."> 等は CSP でブロックされる。vite-plugin-singlefile で全アセット(JS / CSS / フォント・画像)をインライン化必須。外部フォントを使う場合は base64 エンコードして CSS に埋め込む。
  5. GAS HtmlService CSP: eval 禁止: React 18+ の development mode は eval を使用するため GAS では動かない。vite build --mode production で必ずビルドする。ローカル開発時の vite dev は GAS 環境外で使う。
  6. clasp push と Vite ビルドの順序: CI 未着手のため、手動運用は npm run build:spa && npm run push:dev の順序を厳守。scripts/pre-push-check.shnpm run build:spa を前置し、git statustemplates/sidebar_spa_shell.html の未コミット検知を追加する(MAS-224 CI 完成後は GitHub Actions に移管)。
  7. google.script.run タイムアウト(30/60 秒制限): 重い処理(PDF バッチ OCR など)は GAS 側で時間差トリガー起動に逃がし、クライアントから数秒おきに checkStatus(id) を問い合わせるクライアント側ポーリングで対応(MAS-056 Phase 1 で実装予定の基盤を流用)。Phase 1 (a) の MAS-147 HitL は既に非同期キュー実装済のため、ポーリングロジックを React 側に移植するだけ。
  8. React Router: HashRouter のみ動作: iframe sandbox 制約で pushState / BrowserRouter は使えない。HashRouter#/hitl, #/ops 等)で実装する。ルーティング 1 レベル程度なので複雑さは低い。
  9. Zustand or Jotai の localStorage 永続化制限: ブラウザ localStorage は 10MB 目安だが、GAS iframe sandbox 内では利用可能性に制約あり。状態の永続化は GAS 側(03_sys_params / 専用シート)に任せ、React 側はメモリ内のみで管理する方針とする(v1.0)。
  10. ビルド成果物 commit 必須: templates/sidebar_spa_shell.html はビルド成果物だが git 管理する(clasp push 対象)。ソースとの同期ズレを防ぐため、pre-push hook で build:spa を強制実行し、git status で差分検出する。
  11. 並行稼働期間中の「SPA 版と従来版の二重表示」による操作競合: ユーザーが両方のサイドバーを同時に開いた場合、片方の操作が他方に影響しないよう各 iframe 独立動作とする。GAS 側のロック機構(LockService)に委譲可能。
  12. TypeScript 型エラーを CI なしで見逃すリスク: MAS-224 CI 完成前は pre-push hook で tsc --noEmit を実行し、型エラーを commit 前に検知する。
  13. MAS-217 視認性改善資産の継承: MAS-217 の色分け CSS(data-cat="rpa" 等)は React props(<Section category="rpa">)として移植。クラス名は維持し、並行稼働時の見た目を完全一致させる。
  14. PoC 前提の暫定推奨(v1.2 で一部確定済): ディレクトリ配置は v1.2 で webapp_client/(repo root 直下)採用確定。状態管理ライブラリ(Zustand vs Jotai)・段階移行順序(Stage 1-5)・CSS 戦略は v1.2 時点でも暫定推奨(Stage 2 の MAS-147 v2 HitL 移植時に複雑度を見て確定)。
  15. HtmlService.createHtmlOutputFromFiletemplates/ プレフィックス必須(v1.2 追加・PR #361 実地で判明): 呼び出しは HtmlService.createHtmlOutputFromFile('templates/sidebar_spa_shell') と書く(.html 拡張子省略・templates/ プレフィックス付き)。初回トライ時にプレフィックス忘れで Exception: HTMLファイルが見つかりません エラー発生。clasp push ルートの .claspignore 設計により templates/ 配下のみが GAS にアップロードされるため、相対パス指定には templates/ プレフィックスが必要。
  16. 運用ルール(CI 未整備・v1.2 確定): MAS-224 CI パイプライン完成まで 手動ビルド → commit → push のフローを厳守する。下記 §運用ルールセクション参照。

運用ルール(v1.2 確定 — PR #361 で実地運用開始)

CI 未整備のため、ビルド忘れによる事故を避けるための手動運用フローを定義する。MAS-224 CI パイプライン完成後は GitHub Actions で自動化する(Stage 2-5 の期間中も手動運用を並行)。

Stage 1-5 共通の手動フロー

1. webapp_client/src/ 配下のソースを編集
2. ビルド実行:
   - repo root から: npm run build:spa(Stage 2 で scripts 追加)
   - または: cd webapp_client && npm run build
3. ビルド成果物 templates/sidebar_spa_shell.html を git add + commit(必須)
4. GAS へ push:
   - npm run push:dev (dev 環境)
   - npm run push:prod (prod 環境)
   - or npm run deploy:dev / deploy:prod(Web アプリ URL 更新含む)

事故防止

  • scripts/pre-push-check.sh に検証追加(Stage 2 で実装予定):
    • git status --porcelain templates/sidebar_spa_shell.html が非空なら push 停止
    • cd webapp_client && tsc --noEmit が非ゼロ終了なら push 停止
  • .claspignore の除外設定は PR #361 時点で設定済:
# ルート直下の非 GAS ファイルを包括除外
*.js
*.md
*.txt
*.json
*.html

# GAS ソースディレクトリを復元(否定パターン)
!000_infra/**
!100_config/**
!200_data/**
!300_ui/**
!400_domain/**
!500_import/**
!600_report/**
!800_ops/**
!900_test/**
!templates/**

上記により webapp_client/** / tasks/** / tools/** は包括除外の結果 GAS には一切 push されない(templates/ 配下のビルド成果物のみ GAS に反映)。

Stage 2 以降の CI 連携(予定)

MAS-224(GitHub Actions CI パイプライン)完成時に以下を自動化:

  • PR 作成時に cd webapp_client && npm ci && npm run build 実行
  • ビルド結果と commit 済 templates/sidebar_spa_shell.html の差分検証
  • tsc --noEmit で TypeScript 型エラー検証
  • clasp push 前の pre-flight チェック

エッジケース

実装時に以下 12 件を単体テスト / 動作確認でカバーする。

#条件検知方法期待される挙動ログ出力
1ビルド成果物 templates/sidebar_spa_shell.html が git にコミットされていない状態で clasp pushscripts/pre-push-check.shgit status --porcelain templates/sidebar_spa_shell.html が非空push 停止 + エラーメッセージ「ビルド成果物を commit してください」shell エラー
2Vite bundle サイズが目標 500KB gzip を超過vite build 後に gzip -c templates/sidebar_spa_shell.html | wc -c が 500000 超過pre-push hook で WARN 表示 + 1MB 超過時は push 停止shell warn
3google.script.run の Promise wrapper がエラー(ネットワーク障害・GAS 側例外)withFailureHandler 発火React 側で指数バックオフ 3 回リトライ(1秒 → 2秒 → 4秒)。最終失敗時は StatusBar に赤表示 + 「再試行」ボタンconsole.error + GAS 側 Utils.persistLog('ERROR', ...)
4Zustand / Jotai state を localStorage 永続化した場合の 10MB 超過JSON.stringify(state).length > 10_000_000永続化をスキップ + WARN 表示(v1.0 ではそもそも永続化しない方針)console.warn
5CSP 違反による外部リソース読み込み失敗(開発者が誤って外部 CDN を import)Chrome DevTools Network タブで blocked:csp エラーReact 側はフォント未適用・画像なし等の graceful degradation で継続動作。開発者は DevTools で発見・修正console.error(CSP レポート)
6並行稼働期の「SPA 版と従来版の二重表示」による操作競合ユーザーが両サイドバーで同時に runInvoiceBatchPublic 実行GAS 側 LockService.getScriptLock().tryLock(30000) で直列化 + 2 回目は「他のユーザーが実行中です」エラーUtils.persistLog('WARN', ...)
7CI なし環境での TypeScript 型エラー見逃しpre-push hook の tsc --noEmit が非ゼロ終了push 停止 + 型エラー表示shell エラー
8初回ロード時に getInitialStateForSpa() が GAS 側でエラーwithFailureHandler 発火React 側で「初期化に失敗しました」全画面エラー + 再試行ボタン同上 ERROR
9MENU_DEFINITION が undefined(MAS-217 未実装環境で本案件を先行起動)initialState.menuDefinition == nullFallback 固定リスト(React 側にハードコード)で表示 + WARN バナー同上 WARN
10ユーザーが GAS 関数実行中に再度同じボタンを押下store.currentAction !== null全ボタン disabled(Zustand / Jotai currentAction ステート参照) + トーストなし
11React コンポーネントの render 中に例外発生React Error Boundary 発火<ErrorBoundary> がキャッチ → 「予期せぬエラー」画面 + スタックトレース(開発時のみ)console.error
12N56_ENABLE_SPA=false 設定下で SPA URL に直接アクセスgetInitialStateForSpa() が例外 throwGAS 側エラー画面 or 401 相当レスポンス同上 WARN

実データ検証

Phase 1 (a) MAS-147 v2 HitL サイドバー SPA 化完成後に以下を測定する。

1. ビルド成果物サイズ

  • 目標: gzip 後 < 500KB(シミュレーター系 Phase 2 で増えても gzip 後 1MB 以内)
  • 内訳目安: React 18 runtime 40KB + React Router ~10KB + Zustand ~1.4KB + Tailwind(purge 後)15KB + アプリコード ~50-200KB
  • 測定コマンド: gzip -c templates/sidebar_spa_shell.html | wc -c

2. 初回ロード時間

  • 既存 invoice_hitl_sidebar.html: サイドバー表示完了まで平均 1200ms(N=10 サンプル・464 行の vanilla JS)
  • SPA 版 sidebar_spa_shell.html: < 2000ms を目標(初回 bundle ロード + getInitialStateForSpa('hitl') 1 往復 + React 初期 render 込み)

3. google.script.run 呼び出し数削減率

  • 既存: HitL レビューで PDF 一覧取得 + 各承認操作ごとに 1 往復 × 10 PDF = 11 往復
  • SPA 版: 初回 getInitialStateForSpa('hitl') 1 往復 + 承認アクションごと 1 往復 × 10 = 11 往復(同等)だが、検索・ソート・フィルタ操作がクライアント完結になるため、ユーザー視点の操作回数は 10 倍削減
  • 測定方法: Chrome DevTools Network タブで 1 セッション内の google.script.run POST 数をカウント

4. MAS-147 v2 HitL サイドバー操作 UX の A/B 比較

  • 手動測定項目:
    • PDF リスト描画完了時間
    • 検索ボックス入力 → フィルタ反映時間(目標 < 50ms)
    • 承認ボタンクリック → 「処理中...」表示までの時間(目標 < 16ms)
    • 全体操作フロー(10 件 HitL レビュー完了まで)の総時間
  • 被験者: 主担当者 1 人で従来版 × SPA 版を交互に 3 回計測し、平均値で比較

5. TypeScript 型チェック

  • cd tools/webapp-client && tsc --noEmit が 0 エラーで通る
  • docs/spec/sidebar_api.d.ts の全関数シグネチャが GAS 側 JSDoc と乖離なし(目視確認)

6. Phase 1 Go/No-Go 判定マトリクス

指標目標判定
bundle サイズ gzip< 500KB
初回ロード時間< 2 秒
HitL 承認クリック反応< 100ms
CSP 違反エラー0 件
TypeScript 型エラー0 件
既存機能の回帰0 件

6 項目全て達成 → Go(Phase 1 (b)-(d) へ進む)、1 項目でも未達 → No-Go(v1.1 で設計見直し)。

関連ドキュメント

人間が検討すべき事項

v1.2 で PoC Stage 1(PR #361)により確定済(解消リスト)

#論点v1.2 確定内容
2ディレクトリ命名tools/webapp-client/ vs templates/sidebar_spa/webapp_client/(repo root 直下)採用。理由: templates/ は GAS push 対象ゆえソース混入リスク、tools/ は既存慣習なし。.claspignore で除外済。
8TypeScript 厳格度strict: true の採用粒度)strict: true + noUnusedLocals + noUnusedParameters + noFallthroughCasesInSwitch(Stage 1 で試作成功)。Stage 2 以降も維持推奨。
1(部分確定)状態管理ライブラリ(Zustand vs Jotai vs useState)Stage 1 は useState のみで足りた(PoC は単一コンポーネント)。Zustand/Jotai 採用判断は Stage 2 の MAS-147 v2 HitL 移植時の状態複雑度で決定(複数パネル・グローバル状態が必要なら採用検討)。
9MAS-057 Phase 3 との統合タイミング(MAS-232 完了後 or 並行)PoC 疎通 OK のため並行可(「MAS-232 完了後」の逐次前提を緩和)。MAS-057 Phase 3 着手時に MAS-232 Stage 2 と並行実装を検討。

v1.2 で引き続き保留(Stage 2 以降で判断)

本案件は Stage 2 以降の判断に持ち越す論点を以下に列挙。

  1. 状態管理ライブラリ選定(Stage 2 の複雑度で決定): Stage 1 は useState で足りたが、Stage 2 の MAS-147 v2 HitL サイドバー(464 行・PDF リスト・承認状態・エラーメッセージ等)で複雑化する場合に Zustand(軽量・Jr フレンドリー)を第一候補として導入判断。Jotai は atomic 設計で更に複雑な MAS-056 Chat 時に再評価。

  2. 状態管理ライブラリ選定(Zustand vs Jotai): Zustand を暫定推奨(軽量 1.4KB・TypeScript 型推論が自然・Jr フレンドリー)。Jotai は atomic 設計で MAS-056 Chat の複雑な状態管理に有利な可能性あり。PoC で両方を MAS-147 HitL に試作して決定。N56_STATE_MGMT_LIB キーで切替可能に設計しておく。

  3. ディレクトリ配置(tools/webapp-client/ vs templates/sidebar_spa/: サブパッケージ案(tools/webapp-client/)はルート package.json を汚染しないメリット、templates/sidebar_spa/ は GAS プロジェクト内で完結するメリット。PoC で両構成のビルド・.claspignore 運用負荷を比較する。

  4. ビルド成果物のコミット方針: CI(MAS-224)未完成前提では成果物を main に commit 必須(pre-push hook で強制)。MAS-224 完成後は GitHub Actions でビルドして自動 commit する運用に移行するか検討。

  5. MAS-147 v2 HitL 先行移行の Go/No-Go タイミング: Phase 1 (a) 完成時点で §実データ検証の 6 項目マトリクスで判定。未達の場合 v1.1 で技術選定再検討。

  6. 従来版サイドバーの廃止タイミング: 段階移行完了時(全 4 段階終了後)に旧 templates/*.html を削除するか、永続並存でユーザーに選択肢を残すか。v1.0 では段階移行完了時に全廃止を推奨

  7. CSS 戦略(CSS-in-JS vs Tailwind vs 素 CSS): Tailwind 暫定推奨(CSP 内で静的 CSS 生成可能・bundle サイズ軽量・learning curve 中程度)。Emotion / styled-components は CSP で runtime 生成がブロックされるため不可。素 CSS は MAS-217 既存資産との整合性が取りやすいが、スケーラビリティに欠ける。

  8. 開発時ホットリロード対応(HtmlService 経由 / ハイブリッド開発環境): Vite dev server は GAS 環境外で動作するため、GAS 側の実データを使った開発は clasp push + 手動リロードが必要。(a) mock データで進める運用(v1.0 推奨)vs (b) Vite dev server の Proxy 機能で GAS Web App(doGet/doPost)の CORS を突破し、ローカル開発中でも実データで動作させるハイブリッド構成(v1.1 Minor 反映・PoC で検証候補)の 2 案を比較検討する。(b) は開発体験が劇的に向上するが、CORS 設定と認証フロー(OAuth トークンのブラウザ側保持)の追加実装が必要。Phase 1 (a) PoC 中に (b) の技術検証を並行実施し、成果は v1.1 に反映する。

  9. TypeScript 厳格度(strict: true の採用粒度): Phase 1 は strict: true 全面採用推奨(Jr 採用要件の React/TS 経験優遇で既習想定)。ただし any cast を許容するエスケープハッチルールを設けるか検討。

  10. MAS-057 Phase 3 との統合タイミング: MAS-232 Phase 1 完了後に即 MAS-057 Phase 3 着手する逐次実装 vs Phase 1 (a) PoC 完成時点で MAS-057 Phase 3 並行着手する並列実装のどちらが合理的か。依存関係上は逐次、工数確保上は並列

  11. 統合ワークスペース UI(左右分割・F-57b 候補)を MAS-232 内で扱うか別案件に切出すか: 2026-04-24 会話では別案件(F-57b)として切出方針で合意済。v1.0 は本案件スコープ外として明示し、F-57b 起票時に本案件成果を基盤として活用する設計。

  12. MAS-224 CI 完成前の運用(手動ビルド + commit の事故防止策): pre-push hook で build:spa + tsc --noEmit + git status 検証を強制。MAS-224 完成まで手動運用で忍耐する or Jr 着任後に MAS-224 を Jr の初期タスクとして急ぎ実装するか。

  13. MAS-230(Jr 採用要件)完成前に main 担当者が単独着手する場合の工数見積: Phase 1 (a) PoC のみなら 1 人月(主担当者の副業時間)で達成可能と想定。Phase 1 (b)-(d) まで含めると 3 人月で Jr 着任待ちが理想。PoC 単独実行 vs Jr 着任まで待機のトレードオフ判断。

  14. clasp push の 1 ファイル 1MB 制限への予防策(v1.1 Minor + v1.3 で案 B 削除): GAS は 1 ファイルあたり約 1MB の制限があり、templates/sidebar_spa_shell.html が React + 多数のパネルアセット + インライン画像で 1MB を超過する可能性がある。エッジケース #2 の 500KB 超過 WARN に加え、1MB 到達時のチャンク分割戦略を設計しておく:

    • 案 A(推奨): 機能ごとに Vite のエントリ HTML を複数作成する(Multi-Page Application ビルドへの移行)sidebar_spa_shell_hitl.html / _ops.html / _whatif.html を Vite の MPA 機能で並列ビルドし、GAS 側で view に応じて createHtmlOutputFromFile の対象を切り替える。Vite の標準サポート範囲内で実装可能。
    • 案 B (GAS include で結合): v1.3 で削除vite-plugin-singlefile を使う環境で GAS 側の concat は技術的難易度が過度に高く、React 18 の chunk 分割と整合しない(Gemini v1.3 レビュー指摘)。
    • v1.0 では 500KB 以内に収める方針で進め、1MB 接近時は案 A(MPA)のみを現実的な選択肢として検討。

実装プロンプト(Claude Code 用)

Phase 1 (a) MAS-147 v2 HitL サイドバー SPA 化のみ記載。(b)-(d) は PoC 完了後に別 PR で展開。

Phase 1 (a) 実装プロンプト(Claude Opus 4.7 推奨・Sr 単独着手想定)

## 案件
MAS-232 Phase 1 (a) — MAS-147 v2 HitL サイドバーの SPA 化 PoC
(約 1 人月・Sr 単独着手・MAS-230 Jr 着任待たず PoC 実施)

## 事前調査(必ず Read する)
1. `templates/invoice_hitl_sidebar.html`(464 行)— 既存 HitL レビュー UI 構造・PDF 一覧表示・承認/スキップボタン
2. `100_config/101_sys_config.js:384-393` — `openInvoiceHitlSidebar()` / `doGet(e)` の既存実装
3. `docs/dev/dev_mas-147_invoice_ocr_auto_posting.md` — MAS-147 v2 の HitL 操作フロー・99_error フォルダ運用
4. `docs/_internal/research_prompts/RQ-034_conversational_scenario_ui_result.md` — Vite + React 設計方針
5. `docs/dev/dev_mas-217_sidebar_catalog_and_readability.md` — 色分け CSS 資産
6. `package.json` / `.claspignore` / `scripts/pre-push-check.sh` — ビルドパイプライン統合点
7. `000_infra/002_constants.js` `MENU_DEFINITION` 構造
8. `000_infra/004_utils.js` `Utils.persistLog` シグネチャ

## 実装対象
1. **`tools/webapp-client/` サブパッケージ新設**(暫定推奨・PoC で配置確定):
   - `package.json`(vite + @vitejs/plugin-react + vite-plugin-singlefile + react + react-dom + @types/react + @types/react-dom + typescript + zustand + react-router-dom)
   - `vite.config.ts`(react + viteSingleFile + target ES2020 + production mode 強制 + outDir '../../templates')
   - `tsconfig.json`(strict: true + jsx react-jsx + target ES2020)
   - `README.md`(Jr 向けセットアップ手順・PoC 運用手順)

2. **`tools/webapp-client/src/` コンポーネント**:
   - `main.tsx` / `App.tsx`(`HashRouter` + 初期ロード)
   - `api/gas-bridge.ts`(`runGas<K>()` typed Promise wrapper)
   - `panels/InvoiceHitlPanel.tsx`(既存 HitL UI を React 再実装)
   - `components/Section.tsx` / `ActionButton.tsx` / `StatusBar.tsx`(MAS-217 CSS 資産を props 継承)
   - `state/appStore.ts`(Zustand・initialState + currentAction + statusMessage)

3. **`300_ui/302_spa_bridge.js` 新設**(v1.3 Major 反映・UI 層責務分離・101_sys_config.js への追加は禁止):
   - `getInitialStateForSpa(view)` 新設(`N56_ENABLE_SPA=false && !isDev` で例外 throw)。**末尾 `_` を付けない**(GAS 末尾 `_` = private 扱いで `google.script.run` 不可・failure_patterns #28)
   - `openInvoiceHitlSidebarSpa()` 新設(`HtmlOutput.append('<script>window.SPA_INITIAL_VIEW="hitl";</script>')` でステートレスに view 注入・v1.3 Critical で PropertiesService 方式撤回)
   - `handleSpaDoGet_(e)` 新設(Web アプリ用・`doGet(e)` から委譲される分岐ハンドラ・同じく `append()` で XSS サニタイズ済 view 注入)
   - `_getPublicSysParamsForSpa_()` private helper(機密キー除外フィルタ)
   - **`_setSpaView_` / `_consumeSpaView_` は実装しない**(v1.1 で暫定指定したが v1.3 Critical で race condition 回避のため撤回)

4. **`100_config/101_sys_config.js` 最小編集**(UI 実装は配置しない・v1.3 Major 反映):
   - `doGet(e)` に `view.endsWith('_spa')` 検知で `handleSpaDoGet_(e)` へ委譲する分岐を 3-5 行追加
   - `MENU_DEFINITION` の `📋 サイドバー: 🔧 開発・設定` カテゴリに `{ label: '🧪 HitL レビュー (SPA)', funcName: 'openInvoiceHitlSidebarSpa', description: 'Vite+React SPA 版(N-56 Stage 2・本格)' }` を追加
   - PoC Stage 1 用の `openSpaSidebarPoc` / `spaPocGetEnvInfo` / `spaPocEcho` は Stage 2 完了後に削除(移行期間中は並存)

5. **`docs/spec/sidebar_api.d.ts` 新設**(SSoT・v1.3 で TypeScript 強制型を導入):
   - `JSONSafe<T>` ユーティリティ型を先頭に定義(`T extends Date ? never : ...` で再帰的に Date 拒否)
   - `SidebarApi` interface で `getInitialStateForSpa` / `runInvoiceBatchPublic` / 他を宣言・戻り値は `JSONSafe<T>` で必ずラップ
   - `gas-bridge.ts` から `import type` で参照
   - Stage 2 は HitL 関連関数 + 初期ロードのみ(網羅は Stage 3 以降)

6. **`03_sys_params` に 2 キー追加**:
   - `N56_STATE_MGMT_LIB`(デフォルト `ZUSTAND`)
   - `N56_ENABLE_SPA`(デフォルト `false`・PoC 環境で手動 `true` 設定)

7. **ビルドパイプライン整備**:
   - ルート `package.json` の `scripts` に `build:spa` / `dev:spa` / `type-check:spa` 追加
   - `scripts/pre-push-check.sh` に `npm run build:spa && npm run type-check:spa && git status --porcelain templates/sidebar_spa_shell.html` 検証を前置
   - `.claspignore` で `tools/webapp-client/` 配下を除外(成果物 `templates/sidebar_spa_shell.html` のみ push)

8. **動作確認(§実データ検証の 6 項目)**:
   - bundle サイズ gzip < 500KB
   - 初回ロード < 2 秒
   - HitL 承認クリック反応 < 100ms
   - CSP 違反エラー 0 件
   - TypeScript 型エラー 0 件
   - 既存 `openInvoiceHitlSidebar()` と `openInvoiceHitlSidebarSpa()` の 承認/スキップ動作一致

## Phase 1 (a) デプロイ手順
1. dev 環境で `npm run build:spa` → `npm run push:dev` → `openInvoiceHitlSidebarSpa()` 手動実行
2. §実データ検証 6 項目で Go/No-Go 判定
3. Go なら `npm run push:prod`(prod 環境は `N56_ENABLE_SPA=false` 固定で誰も使えない状態で待機)
4. コミットメッセージ: `feat(N-56 Phase 1 (a)): Vite+React SPA 基盤 + I-03 v2 HitL PoC`

## failure_patterns チェック
- #18-#20: パッケージ名(vite-plugin-singlefile 等)と関数名(getInitialStateForSpa)を Read で裏取り
- #28: `google.script.run` から呼ぶ関数は末尾 `_` を付けない(getInitialStateForSpa は末尾 `_` なし)
- #25: openInvoiceHitlSidebarSpa は既存 openInvoiceHitlSidebar と対称
- #26: appsscript.json は変更なし

推奨実行モデル

Phase / Step推奨モデル根拠
Phase 1 (a) MAS-147 v2 HitL PoCClaude Opus 4.7 (1M context)新規モノレポ基盤構築 + Vite + React + TypeScript + MAS-217 CSS 資産継承 + ビルドパイプライン設計の複合判断。Sr 単独着手推奨
Phase 1 (b) MAS-175 OCR 失敗フォームClaude Sonnet 4.6(a) 確立後のパターン適用
Phase 1 (c) MAS-174 小口現金(Jr 担当想定)Claude Sonnet 4.6Jr 独立担当領域・監督下で実装
Phase 1 (d) MAS-217 既存統合Claude Opus 4.7 (1M context)41 ボタン網羅 + MAS-217 視認性改善の最終統合
Phase 2 以降(MAS-011/MAS-048/MAS-049 シミュレーター SPA 化)Claude Sonnet 4.6パターン化済みの機能別移行
仕様書レビュー(4_review_specs_by_gemini.js)Gemini 3 Pro Preview + Deep Think第三者視点での技術選定 + CSP 制約 + SPA アーキテクチャの深い検証

変更履歴

日時バージョン変更内容
2026-04-27v1.4 (Stage 2 リネーム反映)getInitialStateForSpa_getInitialStateForSpa リネーム反映(main 側 PR #372 / commit da1e549 でマージ済)。Stage 2 dev 投入時にサイドバー初期化エラー(google.script.run.getInitialStateForSpa_ is not a function)で起動不能となり原因調査の結果、GAS 仕様で末尾アンダースコア _ の関数は private 扱いとなりクライアント(google.script.run)から呼び出せない ことが判明。spec / 実装ともに末尾 _ を削除した名前 getInitialStateForSpa に統一。本 spec 内の現在参照(型定義 / 関数定義 / 影響範囲 / 注意事項 / エッジケース / 性能目標 / 実装プロンプト 計 14 箇所)を更新。failure_patterns.md に新規 #28「GAS 末尾 _ = private、google.script.run から呼べない」を追加し、Step 5 / 注意事項 #1 / 実装プロンプト failure_patterns チェックから #28 を参照。v1.1〜v1.3 の changelog 履歴行は当時の名称をそのまま保持(履歴用引用のため)。
2026-04-25 09:45v1.3Gemini 3 Pro Preview レビュー CONDITIONAL GO 指摘 5 件を全反映(Critical 1 + Major 2 + Minor 2・v1.2 PoC Stage 1 反映後の 2 回目レビュー)。🔴 Critical (UserProperties による view 伝達の Race Condition): v1.1 で指定した _setSpaView_ / _consumeSpaView_PropertiesService.getUserProperties() 経由)は、複数タブで別々のサイドバーを立て続けに開いた際にプロパティが上書きされ、誤った view が表示される致命的な競合バグを引き起こす。対応: PropertiesService 経由方式を完全撤回し、HtmlOutput.append('<script>window.SPA_INITIAL_VIEW="...";</script>') でステートレスに JS 変数を注入する方式に変更(各 iframe が独立 window を持つため競合なし・GAS 公式推奨パターン)。handleSpaDoGet_(e) では XSS 予防のため String(view).replace(/[^a-z_0-9-]/g, '') でサニタイズ後に注入。Step 5 の設計原則を更新、今後の全 SPA サイドバーの標準パターンとして確立。🟡 Major (実装プロンプトと v1.1 設計方針の矛盾): 仕様書上部で「UI エンドポイントは 302_spa_bridge.js に集約」と決定しながら、実装プロンプト ## 実装対象 セクション 3 で 101_sys_config.js 拡張 として記述が残存していた問題(AI が旧方針で実装するリスク)。対応: 実装プロンプトを書き換え、セクション 3 を 300_ui/302_spa_bridge.js 新設 に、セクション 4 を 101_sys_config.js 最小編集(doGet 分岐 + MENU_DEFINITION 1 行のみ)に分離。_setSpaView_ / _consumeSpaView_ を実装しない旨を明記。🟡 Major (Date 型禁止の型システム強制): v1.1 でコメント規約のみで定義した Date 型禁止を、TypeScript ユーティリティ型で物理的に弾く仕組みに昇華。対応: sidebar_api.d.tstype JSONSafe<T> = T extends Date ? never : T extends Function ? never : T extends object ? { [K in keyof T]: JSONSafe<T[K]> } : T; を先頭定義し、SidebarApi の全戻り値を JSONSafe<T> でラップ。Jr が誤って Date を含めても tsc --noEmit がコンパイル時に弾く。🟢 Minor (1MB チャンク分割の案 B 削除): v1.1 で提示した「案 B GAS include で結合」は vite-plugin-singlefile と相性が悪く React 18 chunk 分割と整合しない。対応: 人間検討事項 #13 から案 B を削除し、「案 A Multi-Page Application ビルド(Vite MPA 機能で機能別 HTML を並列ビルドし GAS 側で view に応じ切替)」のみを現実的選択肢として明記。🟢 Minor (FOUC 対策): React マウント後に view 判明だと初期 URL / のチラつき(FOUC)発生リスク。対応: main.tsxwindow.SPA_INITIAL_VIEW をマウント前に読み取り、HashRouterinitialEntries に即座に設定する実装例を Step 5 に追加。変更規模: spec 約 700 → 約 750 行。v1.2 で確立した Stage 1 PoC 結果(webapp_client/ 配置・gasRun 名称・templates/ プレフィックス)は維持し、Stage 2 以降の指針のみ修正。CONDITIONAL GO 2 条件全達成(PropertiesService 撤回 / 実装プロンプトの UI 層分離反映)で実装着手可能状態。
2026-04-25 09:00v1.2main 側 PoC Stage 1 実地検証(PR #361 マージ済 commit 274b67d・4/4 検証項目クリア)の確定事項を反映。main 担当者の依頼を受け、v1.0/v1.1 で「暫定推奨」としていた論点の一部が実地で確定したため v1.2 に追従。(I) 採用技術スタック確定: Vite 5 (^5.4.10) / React 18 (^18.3.1) / TypeScript 5 (^5.6.0strict: true) / vite-plugin-singlefile 2 (^2.0.3) / @vitejs/plugin-react 4 (^4.3.4)。ビルド設定 assetsInlineLimit: 100_000_000 + cssCodeSplit: false + inlineDynamicImports: true で外部参照完全排除。実機メトリクス: bundle 145KB / gzip 47KB / 外部 CDN 参照 0 / インライン <script> 1 個 / CSP 違反 0 / google.script.run 疎通 OK / useState 再描画確認。(II) ディレクトリ構成確定: webapp_client/(repo root 直下)採用(v1.0 で暫定推奨していた tools/webapp-client/templates/sidebar_spa/ は不採用・理由: templates 配下はソース混入リスク、tools は本 repo 慣習なし)。(III) Step 1 の位置付け変更: 「ツールチェーン初期セットアップ手順」から「PoC Stage 1 で確立済・Stage 2 以降はこの確定構成を前提に移植する」に再定義。webapp_client/package.json / vite.config.ts / tsconfig.json の実装を全文引用。(IV) Step 3 型安全 API bridge 実装確定: 関数名を v1.0 で書いた runGas から gasRun<T = unknown>(fnName: string, ...args: unknown[]): Promise<T> に修正(PR #361 実装準拠)。gas-bridge.ts 全文を引用(GasScriptRun / GasGlobal interface 定義含む)。sidebar_api.d.ts SSoT は Stage 2 で新設予定(PoC Stage 1 は単一関数 3 個なので未導入)。v1.1 で確立した「Date 型禁止規約」は Stage 2 の新設時に継承。(V) Step 5 GAS エントリ配置の実地追従: v1.1 で 300_ui/302_spa_bridge.js への UI 責務分離を指定したが、PoC Stage 1 では 100_config/101_sys_config.js:423-451openSpaSidebarPoc / spaPocGetEnvInfo / spaPocEcho の 3 関数暫定配置(main 側判断・分離コスト > 利益)。v1.1 の 302_spa_bridge.js 分離は Stage 2 本格エントリ(例: openHitlSidebarSpa / getInitialStateForSpa_ / handleSpaDoGet_)着手時に実施と明記。(VI) 段階移行を Stage 1-5 に精緻化: Stage 1 (PoC 完了 ✅) / Stage 2 (MAS-147 HitL 移植・bundle < 500KB / UX 同等以上) / Stage 3 (MAS-175 実装待ち) / Stage 4 (MAS-174 実装待ち) / Stage 5 (MAS-217 operations 統合・他機能揃ってから)。Stage 3/4 は対象案件自体が未実装なので依存待ち状態。(VII) 注意事項 2 件追加: #15 「HtmlService.createHtmlOutputFromFiletemplates/ プレフィックス必須」(PR #361 実地で判明した落とし穴・プレフィックス忘れで「HTML ファイルが見つかりません」エラー)/ #16 「運用ルール CI 未整備・手動ビルド → commit → push 厳守」。(VIII) 新規セクション「運用ルール」: Stage 1-5 共通の手動フロー(ビルド → commit → push → Stage 2 で scripts/pre-push-check.sh に検証追加)+ .claspignore 確定設定(ルート直下非 GAS ファイル包括除外 + GAS ディレクトリ復元パターン)+ MAS-224 CI 完成時の自動化プラン。(IX) 人間検討事項 3 件を解消: ディレクトリ命名(webapp_client/ 採用)/ TypeScript 厳格度(strict: true 試作成功)/ 状態管理ライブラリ(Stage 1 は useState で足りた・Stage 2 で判断)+ MAS-057 Phase 3 との統合(並行可に変更)。変更規模: spec 約 623 → 約 700 行。PoC 実装との齟齬解消 + Stage 2 以降の実務指針を確定。v1.1 の Gemini レビュー反映事項(evaluate() 撤廃 / UI 層分離 / Date 型禁止)は Stage 2 着手時に適用する Stage 移送タイミングを明記で保留。
2026-04-25 07:30v1.1Gemini 3 Pro Preview レビュー CONDITIONAL GO 指摘 6 件を全反映(Critical 1 + Major 3 + Minor 2)。🔴 Critical (Vite ビルド HTML と GAS テンプレート評価の矛盾) + 🟡 Major (巨大 HTML evaluate 性能劣化): v1.0 Step 5 で指示していた template.initialView = 'hitl'; template.evaluate() 方式は、Vite 静的ビルド成果物に <?= initialView ?> タグが存在しないため値が渡らず、さらに 500KB+ の巨大 HTML に対するテンプレートパースが数秒の遅延を生じるリスク。対応: evaluate() を撤廃し HtmlService.createHtmlOutputFromFile() で静的配信、view 伝達は PropertiesService.getUserProperties() にワンショット書き込み → React マウント後に getInitialStateForSpa_() 経由で読み出しのパターンに変更。_setSpaView_() / _consumeSpaView_() private helper を新設(consume 時に自動削除)。設計原則として「<?= ?> 評価は今後の全 SPA サイドバーで禁止」を v1.1 で確立。🟡 Major (UI 層責務越境): openInvoiceHitlSidebarSpa() / getInitialStateForSpa_() を v1.0 で 100_config/101_sys_config.js(設定層)に配置指示していたが、Modular Monolith の層分離原則違反。対応: 300_ui/302_spa_bridge.js(新設) に SPA 関連 UI エンドポイント群を集約(openInvoiceHitlSidebarSpa / getInitialStateForSpa_ / handleSpaDoGet_ / _setSpaView_ / _consumeSpaView_)。101_sys_config.jsdoGet(e)view.endsWith('_spa') 検知時に handleSpaDoGet_(e) へ委譲のみ(UI ロジックを持たない)。🟡 Major (Date 型シリアライズ乖離): google.script.run を通る Date オブジェクトは自動で文字列化されるため、sidebar_api.d.ts で Date 型を使うと React 側で .getFullYear is not a function 等のランタイムエラー。対応: sidebar_api.d.ts 先頭に「JSON-safe 型のみ許容・Date 型禁止」の規約コメント追記、日時は string (ISO8601 "2026-04-25T06:00:00+09:00") or number (epoch ms) を強制。ファイル Blob も禁止(string base64 or Drive file ID)。🟢 Minor (Vite Proxy + GAS Web App ハイブリッド開発): 人間検討事項 #7 に「(a) mock 運用 vs (b) Vite dev server Proxy で GAS Web App doGet/doPost の CORS を突破する実データ開発」の 2 案比較を追加(PoC 中に並行検証・v1.1 で再評価)。🟢 Minor (clasp push 1MB 制限): 人間検討事項 #13 を新設(1MB 超過時のチャンク分割戦略: 案 A view 別 HTML 分割 or 案 B 共通ライブラリ/アプリコード分割 + GAS concat)。エッジケース #2 の 500KB 超過 WARN は維持しつつ、1MB 到達時の上位警告 + 案 A/B 選択ルートを明示。変更規模: spec 約 490 → 約 530 行。設計変更 2 点(evaluate 撤廃 + UI 層分離)はアーキテクチャ確立レベルの修正で、今後全 SPA サイドバーの標準パターンとなる。CONDITIONAL GO 3 条件(evaluate 撤廃 / UI 層分離 / Date 型禁止)を全て満たし、実装着手可能状態に昇格。
2026-04-25 06:00v1.0初版作成(sub ワークスペース担当・main 側指示ベース)。MAS-217 の続編として、既存サイドバー 6 ファイルを Vite + React + TypeScript の SPA に段階移行する Step 1-5 仕様を定義。PoC 前提の暫定推奨仕様(ディレクトリ配置・状態管理ライブラリ・段階移行順序は v1.1 で確定)。段階移行順序(v1.0 暫定): (a) MAS-147 v2 HitL サイドバー(Phase 1 PoC 本命・先行) → (b) MAS-175 OCR 失敗フォーム → (c) MAS-174 小口現金 → (d) MAS-217 既存 operations_sidebar.html 統合版。Step 1 ツールチェーン初期セットアップ(vite 5 + @vitejs/plugin-react 4 + vite-plugin-singlefile 2 + react 18 + zustand 4 + typescript 5 + react-router-dom 6)、Step 2 ディレクトリ構成(暫定推奨 tools/webapp-client/ or 代替 templates/sidebar_spa/)、Step 3 型安全 API bridge(gas-bridge.ts typed Promise wrapper + docs/spec/sidebar_api.d.ts SSoT)、Step 4 段階移行 Go/No-Go 基準(bundle < 500KB gzip / 初回 < 2 秒 / クリック反応 < 100ms の 6 項目マトリクス)、**Step 5** GAS 側配信エンドポイント(openInvoiceHitlSidebarSpa() + doGet(e) view=hitl_spa 分岐 + N56_ENABLE_SPA フラグ)。**03_sys_params 追加 2 キー**: N56_STATE_MGMT_LIBZUSTAND/JOTAI)+ N56_ENABLE_SPAtrue/false・段階有効化)。**CSP 制約**: 外部 CDN 禁止(vite-plugin-singlefile で全インライン化)+ eval 禁止(production mode 強制)+ React Router は HashRouter のみ動作。**注意事項 14 件** + **エッジケース 12 件**(ビルド成果物未コミット検知 / bundle > 1MB 警告 / Promise wrapper 再試行 / localStorage 10MB 制限 / CSP 違反 / 並行稼働二重表示 / TS 型エラー CI なし)+ 人間検討事項 12 件(状態管理 / ディレクトリ / ビルド成果物 commit / MAS-147 先行 Go/No-Go / 従来版廃止時期 / CSS 戦略 / ホットリロード / TS 厳格度 / MAS-057 Phase 3 統合 / F-57b 切出 / CI なし運用 / Jr 着任前工数)。MAS-056 Phase 1 / MAS-057 Phase 3 / F-57b 候補の共通基盤として位置づけ。前提: MAS-217 仕様書完了 / MAS-224 CI 未着手 / MAS-230 採用要件未着手。main 側 PoC 先行試作を想定し、PoC 結果を v1.1 で反映する柔軟性を保った仕様。Gemini 3 Pro Preview + Claude Sonnet 添削パイプラインで骨格生成 + Claude Opus 4.7 (1M) で自走執筆(2026-04-25)。

仕様書作成プロンプト

本仕様書は以下のパイプラインで生成された:

  1. Gemini 3 Pro Preview + Deep Thinkscripts/1_generate_prompts_gemini.js)でメタプロンプト生成
  2. Claude Sonnet 4.6 添削で実行者視点の具体化
  3. 生成されたプロンプト(tasks/prompts/task_N-56.md)を Claude Opus 4.7 (1M context) が自走実行し、Step 1-5 構成 + PoC 前提の暫定推奨 + MAS-147 HitL 先行移行の順序を確定。
task_N-56.md を展開して表示

本仕様書の作成指示プロンプト全文は tasks/prompts/task_N-56.md を参照。パイプラインの再現性確保のため、仕様書末尾への転載は省略し、Git 履歴上の元ファイルを参照する方針とする。

関連コマンド:

# 再生成する場合
TASK_IDS=MAS-232 node scripts/1_generate_prompts_gemini.js

# Gemini レビュー実行
SPEC_FILES=docs/dev/dev_mas-232_sidebar_spa.md node scripts/4_review_specs_by_gemini.js