diff --git a/README.md b/README.md index 47ab6a8..9b482db 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,132 @@ -# How to setup -## 1. Clone this repository +# LabCode -Clone this repository with following command: +自動実験プロトコル言語プラットフォーム -```bash -git clone git@github.com:fuku-inc/labcode-test-environment.git -``` - -## 2. Clone service repositories +## クイックスタート -Move into `labcode-test-environment` directory, run +### 1. リポジトリのクローン ```bash -bash clone_repositories.sh +git clone --recurse-submodules +cd labcode-test-environment ``` -Then, these repository is cloned: - -1. `labcode-sim` -2. `labcode-log-server` -3. `labcode-web-app` - -## 3. Edit environmental variables - -Copy template file with following command: - -```bash -cp labcode-web-app/app/.env.example labcode-web-app/app/.env -``` - -Then, edit `labcode-web-app/app/.env` and replace `your-google-client-id.apps.googleusercontent.com` with correct client ID. - -## 4. Build containers +### 2. ビルド & 起動 ```bash docker compose build -``` - -## 5. Run containers - -```bash docker compose up -d ``` -## 6. (Only first time) setup database +### 3. 初回ユーザー作成 ```bash -docker compose exec log_server sh -c "python -m define_db.models" +curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" ``` -## 7. Create user +### 4. アクセス -1. Access to http://localhost:8000/docs#/users/create_users__post -2. Click "Try it out" -3. Enter email address to "email" -4. Click "Execute" -5. You will obtain response as follows: -``` -{ - "id": 1, - "email": "example@gmail.com" -} -``` -6. Memo the value of "id" as user ID - -## 8. Create Project - -1. Access to http://localhost:8000/docs#/projects/create_projects__post -2. Click "Try it out" -3. Enter project name to "name" -4. Enter user ID obtained at user creation to "user_id" -5. Click "Execute" -6. You will obtain response as follows: - -```{ - "id": 1, - "name": "test_project", - "user_id": 1, - "created_at": "2025-02-25T10:44:18.911007", - "updated_at": "2025-02-25T10:44:18.911017" -} -``` -6. Memo the value of "id" as project ID. +| サービス | URL | +|---------|-----| +| Web UI | http://localhost:5173 | +| Admin Panel | http://localhost:5173/admin | +| API (Swagger UI) | http://localhost:8000/docs | -## 9. Run experiment +--- -1. Access to http://0.0.0.0:8080/docs#/default/run_experiment_run_experiment_post -2. Click "Try it out" -3. Enter project ID obtained at project creation to "project_id" -4. Enter protocol name to "protocol_name" -5. Enter user ID obtained at user creation to "user_id" -6. Upload `labcode-test-environment/protocol.yaml` to "protocol_yaml" -7. Upload `labcode-test-environment/manipulate.yaml` to "manipulate_yaml" -5. Click "Execute" +## ドキュメント -## 10. Access to Experiment tracking UI +詳細なドキュメントは [docs/](./docs/) フォルダを参照してください。 -Access to http://localhost:5173/ +| ドキュメント | 説明 | +|-------------|------| +| [クイックスタート](./docs/00_quickstart.md) | 詳細なセットアップ手順 | +| [システムアーキテクチャ](./docs/01_architecture.md) | システム構成 | +| [Admin Panel](./docs/02_admin_panel.md) | 管理画面の機能 | +| [API リファレンス](./docs/03_api_reference.md) | API エンドポイント一覧 | +| [アップグレードガイド](./docs/04_migration_guide.md) | 既存環境からのアップグレード | +| [S3ストレージ設定](./docs/05_s3_storage_setup.md) | AWS S3連携設定 | +| [DB自動初期化](./docs/06_db_auto_initialization.md) | データベース初期化の仕組み | +| [リモートアクセス](./docs/07_remote_access.md) | 他PCからのアクセス設定 | -# How to access from another computer on the same network +--- -## 1. Get the IP address of the computer running Experiment tracking ui +## アーキテクチャ -1. Open a terminal -2. Run the following command: +```mermaid +graph TB + subgraph Docker["labcode-test-environment"] + subgraph Frontend["Web UI"] + WebApp["labcode-web-app
React + Vite
Port: 5173"] + end -```bash -ip addr -``` + subgraph Backend["Backend API"] + LogServer["labcode-log-server
FastAPI + SQLite
Port: 8000"] + end -Find the LAN IP address from the output: + subgraph Simulator["Simulator"] + Sim["labcode-sim
Port: 8888"] + end -(example) + subgraph Storage["Storage"] + DB[(SQLite
Database)] + S3[(AWS S3
Optional)] + end + end + WebApp <--> LogServer + LogServer <--> Sim + LogServer --> DB + LogServer --> S3 + Sim --> S3 ``` -1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 - link/ether 00:1a:2b:3c:4d:5e brd ff:ff:ff:ff:ff:ff - inet 192.168.1.5/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0 - valid_lft 86386sec preferred_lft 86386sec -``` -In this example, 192.168.1.5 assigned to the eth0 (wired LAN) interface is the IP address within the LAN. -Look for an IP address with these characteristics: -Usually in the format of 192.168.xxx.xxx, 10.xxx.xxx.xxx, or 172.16.xxx.xxx to 172.31.xxx.xxx -Associated with eth0, eno1, enp0s3 (wired LAN) or wlan0, wlp2s0 (wireless LAN) -Displayed with scope global +--- -## 2. Edit the hosts file on the computer you're using to access the Experiment tracking ui +## コンテナ操作 -### Linux - -#### 1. Edit the hosts file with root or sudo privilleges +```bash +# 停止 +docker compose down -``` -sudo nano /etc/hosts -``` +# 再起動 +docker compose restart -#### 2. Add the hostname and IP address +# ログ確認 +docker compose logs -f -``` -192.168.1.5 labcode-web-app.com +# 再ビルド +docker compose build --no-cache +docker compose up -d ``` -(Replace 192.168.1.5 with the actual IP address of the web app server from the previous step) +--- -### Windows +## トラブルシューティング -#### 1. Open Notepad with administrator privileges +### 「Internal Error」が表示される -Search for "Notepad" from the Start menu -Right-click on it and select "Run as administrator" +ユーザーが作成されていない可能性があります。初回ユーザー作成を実行してください。 + +```bash +curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" +``` -#### 2. Open `C:\Windows\System32\drivers\etc\hosts` +### コンテナが起動しない -#### 3. Add the hostname and IP address +```bash +# ログを確認 +docker compose logs log_server -``` -192.168.1.5 labcode-web-app.com +# 再ビルド +docker compose build --no-cache +docker compose up -d ``` -(Replace 192.168.1.5 with the actual IP address of the web app server from the previous step) +詳細は [docs/00_quickstart.md](./docs/00_quickstart.md) のトラブルシューティングセクションを参照してください。 +--- -## 3. Access to Expeirment tracking ui +## ライセンス -Access to http://labcode-web-app.com:5173 \ No newline at end of file +[LICENSE](./LICENSE) を参照してください。 diff --git a/compose.local.yaml b/compose.local.yaml new file mode 100644 index 0000000..900ebab --- /dev/null +++ b/compose.local.yaml @@ -0,0 +1,45 @@ +# ローカルストレージモード用 Docker Compose設定 +services: + lab_simulator: + build: ./labcode-sim/lab_server + container_name: labcode_lab_simulator + volumes: + - ./labcode-sim/lab_server:/app + - ./labcode-sim/storage:/storage + - local_storage:/data/storage + ports: + - "8080:8080" + env_file: + - ./labcode-log-server/.env.local + networks: + - labcode_network + log_server: + build: ./labcode-log-server + container_name: labcode_log_server + ports: + - "8000:8000" + volumes: + - ./labcode-log-server/app:/app + - ./labcode-log-server/data:/data + - local_storage:/data/storage + env_file: + - ./labcode-log-server/.env.local + command: | + uvicorn main:app --host 0.0.0.0 --reload + networks: + - labcode_network + webapp: + build: ./labcode-web-app + container_name: labcode_experiment_tracking_ui + ports: + - "5173:5173" + networks: + - labcode_network + +networks: + labcode_network: + driver: bridge + +volumes: + local_storage: + driver: local diff --git a/compose.yaml b/compose.yaml index 6edf459..93e3138 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,6 +7,8 @@ services: - ./labcode-sim/storage:/storage ports: - "8080:8080" + env_file: + - ./labcode-log-server/.env networks: - labcode_network log_server: @@ -17,6 +19,8 @@ services: volumes: - ./labcode-log-server/app:/app - ./labcode-log-server/data:/data + env_file: + - ./labcode-log-server/.env command: | uvicorn main:app --host 0.0.0.0 --reload networks: diff --git a/docs/00_quickstart.md b/docs/00_quickstart.md new file mode 100644 index 0000000..8160f2f --- /dev/null +++ b/docs/00_quickstart.md @@ -0,0 +1,207 @@ +# LabCode クイックスタートガイド + +このガイドでは、LabCodeを最低限動かすための手順を説明します。 + +--- + +## 1. 事前準備 + +以下のソフトウェアがインストールされていることを確認してください: + +- **Docker** (v20.10以上) +- **Docker Compose** (v2.0以上) +- **Git** + +--- + +## 2. セットアップ + +### Step 1: リポジトリのクローン + +```bash +git clone --recurse-submodules +cd labcode-test-environment +``` + +### Step 2: Dockerイメージのビルド + +```bash +docker compose build +``` + +### Step 3: コンテナの起動 + +```bash +docker compose up -d +``` + +起動完了まで数秒お待ちください。 + +--- + +## 3. 初回設定(必須) + +**新規インストールの場合**、最初のユーザーを作成する必要があります。 + +```bash +curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" +``` + +※ `your-email@example.com` を実際のメールアドレスに置き換えてください。 + +--- + +## 4. アクセス + +| サービス | URL | +|---------|-----| +| Web UI | http://localhost:5173 | +| Admin Panel | http://localhost:5173/admin | +| API (Swagger UI) | http://localhost:8000/docs | + +--- + +## 5. 基本的な使い方 + +1. **ブラウザで http://localhost:5173 にアクセス** +2. **Googleアカウントでログイン**(作成したユーザーのメールアドレスで) +3. **Run Listページが表示されます** + +### Admin Panelを使う + +1. 画面右上の「Admin」バッジをクリック +2. ユーザー管理、プロジェクト管理、実験実行ができます + +--- + +## 6. コンテナの操作 + +```bash +# 停止 +docker compose down + +# 再起動 +docker compose restart + +# ログ確認 +docker compose logs -f +``` + +--- + +## 7. S3ストレージ設定(オプション) + +S3を使用する場合は、以下の手順で設定してください。 + +### Step 1: S3バケットの作成 + +1. [AWS Management Console](https://console.aws.amazon.com/) にログイン +2. サービス検索で「S3」を検索してS3コンソールを開く +3. 「バケットを作成」をクリック +4. 以下を設定: + - **バケット名**: `labcode-dev-artifacts`(任意の名前に変更可) + - **リージョン**: `アジアパシフィック (東京) ap-northeast-1` + - **パブリックアクセス**: 「パブリックアクセスをすべてブロック」にチェック +5. 「バケットを作成」をクリック + +### Step 2: IAMユーザーとアクセスキーの作成 + +1. AWSコンソールで「IAM」を検索してIAMコンソールを開く +2. 左メニューから「ユーザー」→「ユーザーを作成」 +3. ユーザー名: `labcode-s3-user` +4. 「許可を設定」→「ポリシーを直接アタッチする」→「ポリシーを作成」 +5. 「JSON」タブで以下を貼り付け: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::labcode-dev-artifacts", + "arn:aws:s3:::labcode-dev-artifacts/*" + ] + } + ] +} +``` + +6. ポリシー名: `LabCodeS3Policy` →「ポリシーを作成」 +7. 作成したポリシーをユーザーにアタッチして「ユーザーを作成」 +8. ユーザーを選択 →「セキュリティ認証情報」→「アクセスキーを作成」 +9. **アクセスキーID**と**シークレットアクセスキー**をメモ + +### Step 3: 環境変数ファイルの作成 + +`labcode-log-server/.env` を作成: + +```bash +AWS_ACCESS_KEY_ID=<取得したアクセスキーID> +AWS_SECRET_ACCESS_KEY=<取得したシークレットアクセスキー> +AWS_DEFAULT_REGION=ap-northeast-1 +S3_BUCKET_NAME=labcode-dev-artifacts +``` + +### Step 4: コンテナの再起動 + +```bash +docker compose down +docker compose up -d +``` + +詳細は [S3ストレージ設定手順書](./05_s3_storage_setup.md) を参照してください。 + +--- + +## 8. トラブルシューティング + +### 「Internal Error」が表示される + +**原因**: ユーザーが作成されていない + +**解決策**: Step 3の初回設定を実行してください。 + +```bash +curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" +``` + +### コンテナが起動しない + +**解決策**: + +```bash +# ログを確認 +docker compose logs log_server + +# 再ビルド +docker compose build --no-cache +docker compose up -d +``` + +### ポートが使用中 + +**解決策**: 他のサービスが5173, 8000, 8888ポートを使用していないか確認してください。 + +```bash +lsof -i :5173 +lsof -i :8000 +lsof -i :8888 +``` + +--- + +## 関連ドキュメント + +詳細な情報については、以下のドキュメントを参照してください: + +- [システムアーキテクチャ](./01_architecture.md) +- [Admin Panel機能仕様](./02_admin_panel.md) +- [APIリファレンス](./03_api_reference.md) +- [S3ストレージ設定](./05_s3_storage_setup.md) diff --git a/docs/01_architecture.md b/docs/01_architecture.md new file mode 100644 index 0000000..1c4c055 --- /dev/null +++ b/docs/01_architecture.md @@ -0,0 +1,124 @@ +# システムアーキテクチャ + +## 概要 + +LabCodeは4つの主要コンポーネントで構成されるシステムです。 + +## システム構成図 + +```mermaid +graph TB + subgraph Docker["LabCode システム (Docker Compose環境)"] + subgraph Frontend["Web UI"] + WebApp["labcode-web-app
React + Vite
Port: 5173"] + end + + subgraph Backend["Backend API"] + LogServer["labcode-log-server
FastAPI + SQLAlchemy
Port: 8000"] + end + + subgraph Simulator["Simulator"] + Sim["labcode-sim
Port: 8888"] + end + + subgraph Storage["Storage Layer"] + DB[(SQLite
Database)] + S3[(AWS S3
Optional)] + end + end + + WebApp <--> LogServer + LogServer <--> Sim + LogServer --> DB + LogServer --> S3 + Sim --> S3 + + style Frontend fill:#e3f2fd + style Backend fill:#fff3e0 + style Simulator fill:#f3e5f5 + style Storage fill:#e8f5e9 +``` + +## コンポーネント + +### Web UI (Port: 5173) + +ブラウザからアクセスするWebアプリケーション。 + +**主要機能**: +- ユーザー認証(Google OAuth) +- Run一覧表示・詳細表示 +- DAGビューア(実験プロセス可視化) +- ファイルブラウザ・ダウンロード +- Admin Panel(ユーザー・プロジェクト管理、実験実行) + +### Backend API (Port: 8000) + +REST APIを提供するバックエンドサーバー。 + +**主要機能**: +- ユーザー・プロジェクト管理 +- Run(実験実行)管理 +- プロセス・オペレーション・ポート管理 +- ストレージ連携(S3/ローカル) + +### Simulator (Port: 8888) + +実験シミュレータ。 + +**主要機能**: +- プロトコルファイル(YAML)の解析・実行 +- 実験ログの生成・送信 +- ストレージへのデータ書き込み + +### PostgreSQL (Port: 5432) + +データベース。ユーザー、プロジェクト、Run、プロセスなどのデータを永続化。 + +### AWS S3 (Optional) + +実験データのストレージ。ローカルストレージも使用可能。 + +## 通信フロー + +```mermaid +sequenceDiagram + participant Browser as ブラウザ + participant WebUI as Web UI
(5173) + participant API as Backend API
(8000) + participant DB as SQLite + participant Sim as Simulator
(8888) + participant Storage as S3 / ローカル + + Browser->>WebUI: HTTP Request + + alt /api/* リクエスト + WebUI->>API: Proxy + API->>DB: Query + DB-->>API: Result + API-->>WebUI: Response + end + + alt /sim_api/* リクエスト + WebUI->>Sim: Proxy + Sim->>Storage: Read/Write + Storage-->>Sim: Data + Sim-->>WebUI: Response + end + + WebUI-->>Browser: HTTP Response +``` + +## アクセス方法 + +| サービス | URL | +|---------|-----| +| Web UI | http://localhost:5173 | +| Admin Panel | http://localhost:5173/admin | +| API (Swagger UI) | http://localhost:8000/docs | + +## 環境変数 + +| 変数名 | 説明 | デフォルト | +|--------|------|-----------| +| `VITE_FEATURE_ADMIN_PANEL` | Admin Panelの有効/無効 | `true` | diff --git a/docs/02_admin_panel.md b/docs/02_admin_panel.md new file mode 100644 index 0000000..8f08bb8 --- /dev/null +++ b/docs/02_admin_panel.md @@ -0,0 +1,103 @@ +# Admin Panel 機能仕様 + +## 概要 + +Admin Panelは、ユーザー管理・プロジェクト管理・実験実行を行うための管理画面です。 + +## アクセス方法 + +- URL: `/admin` +- Run Listページのヘッダーに「Admin」バッジが表示される +- 環境変数 `VITE_FEATURE_ADMIN_PANEL=true` で有効化 + +## ページ構成 + +### 1. Dashboard (`/admin`) + +**機能**: +- 統計情報の表示(ユーザー数、プロジェクト数) +- Quick Actions(新規ユーザー作成、新規プロジェクト作成、実験実行、Run List表示) +- Getting Started ガイド + +**画面レイアウト**: +``` +┌─────────────────────────────────────────────────────────────┐ +│ [Sidebar] │ Dashboard │ +│ ├──────────────────────────────────────────┤ +│ Admin │ Welcome to Admin Panel │ +│ ├ Dashboard │ │ +│ ├ Users │ ┌────────┐ ┌────────┐ ┌────────┐ │ +│ ├ Projects │ │ Users │ │Projects│ │ Runs │ │ +│ └ Run Experiment│ │ N │ │ N │ │ - │ │ +│ │ └────────┘ └────────┘ └────────┘ │ +│ Main App │ │ +│ └ Run List │ Quick Actions: [New User] [New Project] │ +│ │ [Run Experiment] [Run List]│ +│ ─────────────────│ │ +│ [Back to Main] │ Getting Started: │ +│ │ 1. Create a user │ +│ │ 2. Create a project │ +│ │ 3. Run an experiment │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2. Users (`/admin/users`) + +**機能**: +- ユーザー一覧表示 +- 新規ユーザー作成(メールアドレスを入力) +- ユーザー削除(確認ダイアログ付き) +- 各ユーザーのプロジェクト一覧表示 + +### 3. Projects (`/admin/projects`) + +**機能**: +- プロジェクト一覧表示(所有者情報付き) +- 新規プロジェクト作成(名前と所有者を選択) +- プロジェクト編集 +- プロジェクト削除(確認ダイアログ付き) + +### 4. Run Experiment (`/admin/experiments/run`) + +4ステップのウィザード形式で実験を実行します。 + +**ステップ1: Project Select** +- プロジェクトを選択 +- 実行ユーザーを選択(プロジェクト所有者) + +**ステップ2: Config** +- プロトコル名を入力 +- protocol.yaml ファイルをアップロード +- manipulate.yaml ファイルをアップロード(任意) + +**ステップ3: Running** +- 実行中のプログレス表示 +- ログ出力のリアルタイム表示 + +**ステップ4: Complete** +- 成功/失敗の結果表示 +- Run ID、所要時間表示 +- 結果表示またはRun Listへのナビゲーション + +## 注意事項 + +### 実行ユーザーについて + +Admin Panelで実験を実行する際、**選択したユーザー**がRunの所有者になります。 + +- ログインユーザーと実行ユーザーが異なる場合、完了後に「View Results」ボタンは表示されません +- 代わりに警告メッセージと「Go to My Run List」ボタンが表示されます +- 実行したRunを確認するには、所有者ユーザーとしてログインする必要があります + +### Feature Flag + +Admin Panelは環境変数で無効化できます: + +```bash +# Admin Panelを無効化 +VITE_FEATURE_ADMIN_PANEL=false +``` + +無効化すると: +- Run Listページの「Admin」バッジが非表示になります +- `/admin` URLにアクセスしてもAdmin Panelは表示されません diff --git a/docs/03_api_reference.md b/docs/03_api_reference.md new file mode 100644 index 0000000..df29208 --- /dev/null +++ b/docs/03_api_reference.md @@ -0,0 +1,81 @@ +# API リファレンス + +## 概要 + +LabCodeが提供するREST APIの一覧です。 + +- **Base URL**: `http://localhost:8000` +- **Swagger UI**: `http://localhost:8000/docs` + +## ユーザー管理 API + +| Method | Path | 説明 | +|--------|------|------| +| GET | `/users/list` | 全ユーザー一覧取得 | +| GET | `/users/{id}/projects` | ユーザーのプロジェクト一覧 | +| POST | `/users` | ユーザー作成 | +| DELETE | `/users/{id}` | ユーザー削除 | + +## プロジェクト管理 API + +| Method | Path | 説明 | +|--------|------|------| +| GET | `/projects/list` | 全プロジェクト一覧取得 | +| GET | `/projects/{id}` | プロジェクト詳細取得 | +| POST | `/projects` | プロジェクト作成 | +| PUT | `/projects/{id}` | プロジェクト更新 | +| DELETE | `/projects/{id}` | プロジェクト削除 | + +## Run API + +| Method | Path | 説明 | +|--------|------|------| +| GET | `/runs` | Run一覧取得 | +| GET | `/runs/{id}` | Run詳細取得 | +| PATCH | `/runs/{id}/visibility` | 表示/非表示切り替え | + +## ストレージ API + +| Method | Path | 説明 | +|--------|------|------| +| GET | `/v2/storage/info/{run_id}` | Runのストレージ情報取得 | +| GET | `/v2/storage/list/{run_id}` | Runのファイル一覧取得 | +| GET | `/v2/storage/download/{run_id}` | ファイルダウンロード | +| POST | `/v2/storage/download-batch/{run_id}` | 一括ダウンロード | + +## 実験実行 API + +実験実行はシミュレータAPI経由で行います。 + +| Method | Path | 説明 | +|--------|------|------| +| POST | `/sim_api/run` | 実験実行 | + +### 実験実行リクエスト + +- Content-Type: `multipart/form-data` +- パラメータ: + - `protocol_file`: protocol.yaml ファイル(必須) + - `manipulate_file`: manipulate.yaml ファイル(任意) + - `user_email`: 実行ユーザーのメールアドレス + - `project_id`: プロジェクトID + - `protocol_name`: プロトコル名 + +## HTTPステータスコード + +| コード | 説明 | +|--------|------| +| 200 | 成功 | +| 201 | 作成成功 | +| 400 | リクエスト不正 | +| 403 | アクセス拒否 | +| 404 | リソースなし | +| 500 | サーバーエラー | + +## 詳細なAPI仕様 + +詳細なAPI仕様はSwagger UIで確認できます: + +``` +http://localhost:8000/docs +``` diff --git a/docs/04_migration_guide.md b/docs/04_migration_guide.md new file mode 100644 index 0000000..0db2fa6 --- /dev/null +++ b/docs/04_migration_guide.md @@ -0,0 +1,303 @@ +# アップグレードガイド + +## 概要 + +このドキュメントは、LabCodeの新バージョンへのアップグレード手順を説明します。 + +## 新機能サマリー + +### S3ストレージ連携 + +- **Storage v2 API**: S3/ローカル統合ストレージ +- **storage_mode**: Run毎にS3/ローカルを自動判定 +- **ファイルブラウザ**: S3上のファイルを直接閲覧・ダウンロード + +### Admin Panel + +- **ユーザー管理**: ユーザーの作成・削除 +- **プロジェクト管理**: プロジェクトの作成・編集・削除 +- **実験実行**: 4ステップウィザードで実験を実行 +- **ダッシュボード**: 統計情報とクイックアクション + +### その他の改善 + +- **Forbidden ページ**: 詳細説明とナビゲーション追加 +- **ナビゲーション**: Admin ↔ Run List 相互リンク +- **Feature Flag**: 機能の有効/無効を環境変数で制御 + +## アップグレードシナリオ + +お使いの環境に応じて、適切なアップグレードパスを選択してください。 + +| シナリオ | 説明 | マイグレーション | 初回ユーザー作成 | +|---------|------|-----------------|-----------------| +| A | S3導入前 → 最新版 | 必要(Step 4を実行) | 不要 | +| B | S3導入後 → 最新版(Admin Panel追加) | 不要 | 不要 | +| C | 新規インストール | 不要(自動) | 必要(Step 5を実行) | + +## 共通手順 + +### 前提条件 + +- Docker と Docker Compose がインストールされていること +- 既存のLabCode環境が動作していること + +### Step 1: リポジトリの更新 + +```bash +# labcode-test-environment ディレクトリへ移動 +cd /path/to/labcode-test-environment + +# 最新バージョンを取得 +git pull + +# サブモジュールを更新 +git submodule update --init --recursive +``` + +### Step 2: 環境変数の設定 + +`.env` ファイルに以下の変数を設定してください: + +```bash +# Admin Panelの有効化 +VITE_FEATURE_ADMIN_PANEL=true + +# S3設定(S3を使用する場合) +AWS_ACCESS_KEY_ID=your_access_key +AWS_SECRET_ACCESS_KEY=your_secret_key +AWS_REGION=ap-northeast-1 +S3_BUCKET_NAME=your-bucket-name +``` + +### Step 3: Docker イメージの再ビルド + +```bash +# すべてのコンテナを停止 +docker compose down + +# イメージを再ビルド +docker compose build + +# コンテナを起動 +docker compose up -d +``` + +### Step 4: データベースマイグレーション(シナリオAのみ) + +**シナリオA(S3導入前 → 最新版)の場合のみ**、以下のマイグレーションを実行してください。 + +**シナリオB(S3導入後 → 最新版)の場合はスキップしてください。** Admin Panelの追加によるデータベース変更はありません。 + +**冪等性について**: すべてのマイグレーションスクリプトは冪等性があり、何度実行しても安全です。 + +| スクリプト | 説明 | +|-----------|------| +| テーブル作成(create_all) | 既存テーブルはスキップ | +| add_process_type_column.py | 既存カラムはスキップ | +| migrate_ports.py | 既存ポート/接続はスキップ | + +#### 4.1 テーブル作成(新規テーブル) + +新しいテーブル(ports, process_operations, port_connections)が自動作成されます。 +アプリケーション起動時に `Base.metadata.create_all()` により作成されます。 + +#### 4.2 process_typeカラムの追加 + +Processテーブルに `process_type` カラムを追加し、既存データを移行します: + +```bash +docker exec labcode_log_server python /app/scripts/add_process_type_column.py +``` + +このスクリプトは: +1. `process_type` カラムを追加 +2. 既存Processレコードの `process_type` を protocol.yaml から取得して設定 +3. 結果を検証 + +#### 4.3 ポートデータの移行 + +既存RunのポートデータをDBに移行します: + +```bash +# まずドライランで確認 +docker exec labcode_log_server python /app/scripts/migrate_ports.py --dry-run + +# 実行 +docker exec labcode_log_server python /app/scripts/migrate_ports.py +``` + +**注**: 既に存在するポートはスキップされるため、何度実行しても安全です。 + +#### 4.4 storage_addressの移行(オプション) + +Google Drive URLをS3パスに移行する場合: + +```bash +# まずドライランで確認 +docker exec labcode_log_server python /app/scripts/migrate_storage_address.py --dry-run + +# 実行 +docker exec labcode_log_server python /app/scripts/migrate_storage_address.py +``` + +### Step 5: 初回ユーザー作成(新規インストール時のみ) + +**新規インストールの場合**、データベースにユーザーが存在しないため、最初のユーザーを作成する必要があります。 + +既存環境をアップグレードする場合は、ユーザーデータは保持されているためスキップしてください。 + +**方法1: API経由(推奨)** + +```bash +curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" +``` + +**方法2: Admin Panel経由** + +1. `http://localhost:5173/admin/users` にアクセス(ログインなしでアクセス可能) +2. 「Create User」ボタンをクリック +3. メールアドレスを入力して作成 + +### Step 6: 動作確認 + +1. ブラウザで `http://localhost:5173` にアクセス +2. ログイン後、Run Listページが表示されることを確認 +3. ヘッダーの「Admin」バッジをクリックしてAdmin Panelにアクセス +4. S3連携が有効な場合、Run詳細でファイルブラウザが表示されることを確認 + +## シナリオ別チェックリスト + +### シナリオA: S3導入前 → 最新版 + +- [ ] Step 1: リポジトリの更新 +- [ ] Step 2: 環境変数の設定(S3設定 + Admin Panel設定) +- [ ] Step 3: Dockerイメージの再ビルド +- [ ] Step 4.1: 新規テーブル作成(自動) +- [ ] Step 4.2: process_typeカラム追加スクリプト実行 +- [ ] Step 4.3: ポートデータ移行スクリプト実行 +- [ ] Step 4.4: storage_address移行(オプション) +- [ ] Step 6: 動作確認 + +### シナリオB: S3導入後 → 最新版(Admin Panel追加) + +- [ ] Step 1: リポジトリの更新 +- [ ] Step 2: 環境変数の設定(`VITE_FEATURE_ADMIN_PANEL=true`のみ) +- [ ] Step 3: Dockerイメージの再ビルド +- [ ] Step 6: 動作確認 + +### シナリオC: 新規インストール + +- [ ] Step 1: リポジトリの取得 +- [ ] Step 2: 環境変数の設定(S3設定 + Admin Panel設定) +- [ ] Step 3: Dockerイメージのビルド・起動 +- [ ] Step 5: 初回ユーザー作成 +- [ ] Step 6: 動作確認 + +**注意**: シナリオBではデータベースマイグレーションは不要です。Admin Panelは既存のテーブルに対するAPI操作(SELECT/JOIN)のみを追加しています。 + +## トラブルシューティング + +### 新規インストール後に「Internal Error」が表示される + +**原因**: データベースにユーザーが存在しない + +新規インストールではテーブルは自動作成されますが、初期ユーザーは作成されません。 +ログインに使用するメールアドレスに対応するユーザーが存在しないため、エラーが発生します。 + +**解決策**: +1. Step 5の手順に従って初回ユーザーを作成してください: + ```bash + curl -X POST "http://localhost:8000/api/users/" -d "email=your-email@example.com" + ``` + +### マイグレーションスクリプトが失敗する + +**原因**: データベースに接続できない + +**解決策**: +1. PostgreSQLコンテナが起動していることを確認: + ```bash + docker compose ps + ``` +2. log_serverコンテナ内から実行: + ```bash + docker exec -it labcode_log_server bash + python scripts/add_process_type_column.py + ``` + +### Admin ボタンが表示されない + +**原因**: Feature Flag が無効になっている + +**解決策**: +1. `.env` ファイルに `VITE_FEATURE_ADMIN_PANEL=true` を追加 +2. コンテナを再ビルド: + ```bash + docker compose build webapp + docker compose up -d webapp + ``` + +### S3からファイルが取得できない + +**原因**: AWS認証情報が設定されていない + +**解決策**: +1. `.env` ファイルにAWS認証情報を設定 +2. S3バケットのCORS設定を確認 +3. コンテナを再起動: + ```bash + docker compose restart + ``` + +### process_typeがNULLのまま + +**原因**: protocol.yamlにアクセスできない + +これは以下の場合に発生します: +- storage_addressがリモートURL +- protocol.yamlが存在しない +- Process名がYAML内のoperations.idと一致しない + +新しく作成されるRunでは自動的に設定されるため、既存データについては対応不要です。 + +## ロールバック手順 + +問題が発生した場合、以前のバージョンに戻すには: + +```bash +# 以前のバージョンに戻す +git checkout <以前のタグまたはブランチ> +git submodule update --init --recursive + +# コンテナを再ビルド +docker compose build +docker compose up -d +``` + +**注意**: データベースマイグレーションを実行した場合、追加されたカラムやテーブルはそのまま残りますが、アプリケーションの動作には影響しません。 + +## 互換性 + +| 項目 | 互換性 | +|-----|-------| +| API | 完全互換(新エンドポイント追加のみ) | +| データベース | 下位互換(新カラム/テーブル追加) | +| 既存機能 | Feature Flagで制御可能 | + +## 次のステップ + +アップグレード完了後: + +1. Admin Panelでユーザーとプロジェクトを管理 +2. 実験実行ウィザードでプロトコルをアップロードして実行 +3. Run Listで結果を確認 +4. S3を使用している場合、ファイルブラウザでデータを確認 + +詳細な使用方法は [Admin Panel仕様](./02_admin_panel.md) を参照してください。 + +--- + +## 関連ドキュメント + +- [データベース自動初期化ガイド](./06_db_auto_initialization.md) - 起動時の自動DB初期化機能 diff --git a/docs/05_s3_storage_setup.md b/docs/05_s3_storage_setup.md new file mode 100644 index 0000000..b6a7783 --- /dev/null +++ b/docs/05_s3_storage_setup.md @@ -0,0 +1,275 @@ +# S3ストレージ設定手順書 + +## 概要 + +LabCodeシステムでAWS S3をストレージバックエンドとして使用するための設定手順を説明します。 + +## 前提条件 + +- AWSアカウントを持っていること +- AWS Management Consoleへのアクセス権があること +- Docker/Docker Composeがインストールされていること + +--- + +## 1. AWS S3バケットの作成 + +### 1.1 AWSコンソールにログイン + +1. [AWS Management Console](https://console.aws.amazon.com/) にアクセス +2. ログイン後、サービス検索で「S3」を検索してS3コンソールを開く + +### 1.2 バケットの作成 + +1. 「バケットを作成」ボタンをクリック + +2. **一般的な設定** + - バケット名: `labcode-dev-artifacts`(任意の名前に変更可) + - AWSリージョン: `アジアパシフィック (東京) ap-northeast-1`(推奨) + +3. **オブジェクト所有者** + - 「ACL無効(推奨)」を選択 + +4. **このバケットのブロックパブリックアクセス設定** + - 「パブリックアクセスをすべてブロック」にチェック(推奨) + +5. **バケットのバージョニング** + - 「無効にする」(任意) + +6. **デフォルトの暗号化** + - 「Amazon S3マネージドキー (SSE-S3)」を選択 + +7. 「バケットを作成」をクリック + +--- + +## 2. IAMユーザーの作成とアクセスキー発行 + +### 2.1 IAMコンソールにアクセス + +1. AWSコンソールで「IAM」を検索してIAMコンソールを開く +2. 左メニューから「ユーザー」を選択 +3. 「ユーザーを作成」をクリック + +### 2.2 ユーザーの作成 + +1. **ユーザー詳細** + - ユーザー名: `labcode-s3-user`(任意) + - 「次へ」をクリック + +2. **許可を設定** + - 「ポリシーを直接アタッチする」を選択 + - 「ポリシーを作成」をクリック(新しいタブが開く) + +### 2.3 カスタムポリシーの作成 + +1. 「JSON」タブを選択 +2. 以下のポリシーを貼り付け(バケット名を適宜変更): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LabCodeS3Access", + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": [ + "arn:aws:s3:::labcode-dev-artifacts", + "arn:aws:s3:::labcode-dev-artifacts/*" + ] + } + ] +} +``` + +3. 「次へ」をクリック +4. ポリシー名: `LabCodeS3Policy` +5. 「ポリシーを作成」をクリック + +### 2.4 ポリシーをユーザーにアタッチ + +1. 元のユーザー作成画面に戻る +2. 作成した`LabCodeS3Policy`を検索して選択 +3. 「次へ」→「ユーザーを作成」をクリック + +### 2.5 アクセスキーの発行 + +1. 作成したユーザー(`labcode-s3-user`)をクリック +2. 「セキュリティ認証情報」タブを選択 +3. 「アクセスキーを作成」をクリック +4. ユースケース: 「コマンドラインインターフェイス (CLI)」を選択 +5. 確認チェックボックスにチェックを入れて「次へ」 +6. 「アクセスキーを作成」をクリック +7. **重要**: `アクセスキーID`と`シークレットアクセスキー`をメモ(この画面を閉じると二度と表示されない) + +--- + +## 3. LabCode環境設定 + +### 3.1 環境変数ファイルの作成 + +`labcode-test-environment/labcode-log-server/.env` を作成または編集: + +```bash +# S3ストレージ設定 +AWS_ACCESS_KEY_ID=<取得したアクセスキーID> +AWS_SECRET_ACCESS_KEY=<取得したシークレットアクセスキー> +AWS_DEFAULT_REGION=ap-northeast-1 +S3_BUCKET_NAME=labcode-dev-artifacts +``` + +### 3.2 設定例 + +```bash +AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXXXX +AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +AWS_DEFAULT_REGION=ap-northeast-1 +S3_BUCKET_NAME=labcode-dev-artifacts +``` + +### 3.3 環境変数一覧 + +| 環境変数 | 説明 | 必須 | デフォルト値 | +|---------|------|------|-------------| +| `AWS_ACCESS_KEY_ID` | IAMアクセスキーID | Yes | - | +| `AWS_SECRET_ACCESS_KEY` | IAMシークレットアクセスキー | Yes | - | +| `AWS_DEFAULT_REGION` | AWSリージョン | No | ap-northeast-1 | +| `S3_BUCKET_NAME` | S3バケット名 | No | labcode-dev-artifacts | +| `S3_ENDPOINT_URL` | S3互換エンドポイント(MinIO等) | No | - | +| `STORAGE_MODE` | ストレージモード (`s3` or `local`) | No | s3 | + +--- + +## 4. 動作確認 + +### 4.1 Dockerコンテナの起動 + +```bash +cd labcode-test-environment +docker compose up -d +``` + +### 4.2 S3接続テスト + +```bash +# コンテナ内でPythonシェルを起動 +docker exec -it labcode_log_server python + +# 以下のコードを実行 +>>> from services.storage_service import get_storage +>>> storage = get_storage() +>>> print(storage.list_objects("runs/")) +# 空リストまたはオブジェクト一覧が表示されればOK +``` + +### 4.3 API経由での確認 + +```bash +# ストレージ情報取得(Run ID 1の場合) +curl http://localhost:8000/api/v2/storage/info/1 +``` + +--- + +## 5. ローカルストレージモード(オプション) + +S3を使用せずにローカルストレージのみで動作させる場合: + +### 5.1 環境変数ファイル(.env.local) + +```bash +# ローカルモード設定 +STORAGE_MODE=local +LOCAL_STORAGE_PATH=/data/storage + +# ダミーS3設定(ローカルモードでは使用されない) +AWS_ACCESS_KEY_ID=dummy +AWS_SECRET_ACCESS_KEY=dummy +AWS_DEFAULT_REGION=ap-northeast-1 +S3_BUCKET_NAME=labcode-dev-artifacts +``` + +### 5.2 Docker Compose起動 + +```bash +docker compose -f compose.local.yaml up -d +``` + +--- + +## 6. トラブルシューティング + +### 6.1 「AccessDenied」エラー + +**原因**: IAMポリシーが正しく設定されていない + +**解決策**: +1. IAMユーザーに正しいポリシーがアタッチされているか確認 +2. ポリシーのリソースARNがバケット名と一致しているか確認 +3. バケットのブロックパブリックアクセス設定を確認 + +### 6.2 「NoSuchBucket」エラー + +**原因**: バケットが存在しない、またはリージョンが異なる + +**解決策**: +1. バケット名のスペルミスを確認 +2. `AWS_DEFAULT_REGION`がバケットのリージョンと一致しているか確認 + +### 6.3 「InvalidAccessKeyId」エラー + +**原因**: アクセスキーIDが無効 + +**解決策**: +1. `.env`ファイルのアクセスキーIDを確認 +2. IAMコンソールでアクセスキーが有効か確認 +3. アクセスキーを再発行 + +### 6.4 接続タイムアウト + +**原因**: ネットワーク接続の問題 + +**解決策**: +1. インターネット接続を確認 +2. プロキシ設定を確認(企業ネットワークの場合) +3. AWS VPCエンドポイントの設定を確認(VPC内の場合) + +--- + +## 7. セキュリティ推奨事項 + +1. **アクセスキーの定期ローテーション** + - 90日ごとにアクセスキーを更新することを推奨 + +2. **最小権限の原則** + - 必要最小限のS3操作権限のみを付与 + +3. **環境変数ファイルの保護** + - `.env`ファイルを`.gitignore`に追加 + - 本番環境ではAWS Secrets Managerの使用を検討 + +4. **バケットポリシーの確認** + - パブリックアクセスを無効化 + - 必要に応じてバケットポリシーでIPアドレス制限を追加 + +--- + +## 8. 参考リンク + +- [AWS S3ドキュメント](https://docs.aws.amazon.com/s3/) +- [IAMユーザーガイド](https://docs.aws.amazon.com/IAM/latest/UserGuide/) +- [boto3 S3ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html) + +--- + +## 関連ドキュメント + +- [アップグレードガイド](./04_migration_guide.md) - S3導入時のマイグレーション手順 +- [システムアーキテクチャ](./01_architecture.md) - ストレージ連携の概要 diff --git a/docs/06_db_auto_initialization.md b/docs/06_db_auto_initialization.md new file mode 100644 index 0000000..cd7cff2 --- /dev/null +++ b/docs/06_db_auto_initialization.md @@ -0,0 +1,465 @@ +# データベース自動初期化ガイド + +## 概要 + +このドキュメントは、LabCodeのデータベース自動初期化機能について説明します。 +FastAPI起動時にデータベースの状態をチェックし、必要に応じて自動的にテーブルを作成・マイグレーションを実行します。 + +## 背景 + +### 問題 + +- データベースファイルが存在しない、または空の場合にAPIがinternal errorを返す +- 手動でのDB初期化が必要で、開発環境セットアップが煩雑 + +### 解決策 + +FastAPI起動時に自動的にデータベースの状態を検証し、必要な初期化を行う仕組みを導入。 + +--- + +## 判定フローチャート + +``` +[コンテナ起動] + ↓ +[uvicorn main:app] + ↓ +[lifespan起動時処理] + ↓ +┌─────────────────────────────────┐ +│ ensure_database_ready() 呼び出し │ +└─────────────────────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ 1. DBファイル存在チェック │ +│ /data/sql_app.db │ +└─────────────────────────────────┘ + ↓ + [存在しない] ──→ create_all() ──→ [完了] + ↓ + [存在する] + ↓ +┌─────────────────────────────────┐ +│ 2. ファイルサイズチェック │ +└─────────────────────────────────┘ + ↓ + [0バイト] ──→ create_all() ──→ [完了] + ↓ + [>0バイト] + ↓ +┌─────────────────────────────────┐ +│ 3. テーブル存在チェック │ +│ SELECT name FROM sqlite_master│ +└─────────────────────────────────┘ + ↓ + [不足あり] ──→ create_all() ──→ カスタムマイグレーション ──→ [完了] + ↓ ↑ + [全て存在] 既存データ保持! + ↓ +┌─────────────────────────────────┐ +│ 4. カスタムマイグレーション │ +│ ALTER TABLE(カラム追加等) │ +└─────────────────────────────────┘ + ↓ +[正常起動] +``` + +--- + +## 動作モード + +| モード | トリガー | 実行内容 | 対象環境 | +|--------|---------|---------|---------| +| 自動 | FastAPI起動時 | create_all() + 軽量マイグレーション | 開発・テスト | +| 手動 | docker exec | 個別スクリプト実行 | 本番 | + +--- + +## 実装詳細 + +### ファイル構成 + +``` +labcode-log-server/ +├── app/ +│ ├── main.py # lifespan コンテキストマネージャ +│ ├── init_db.py # DB初期化モジュール(新規) +│ ├── define_db/ +│ │ ├── database.py # DB接続設定 +│ │ └── models.py # テーブル定義 +│ └── scripts/ +│ ├── add_process_type_column.py # 手動マイグレーション +│ └── migrate_ports.py # 手動マイグレーション +└── data/ + └── sql_app.db # データベースファイル +``` + +### init_db.py + +```python +#!/usr/bin/env python3 +""" +データベース自動初期化・マイグレーションモジュール + +FastAPI起動時に呼び出され: +1. データベースの状態をチェック +2. 必要に応じてテーブルを自動作成(既存データは保持) +3. 必要に応じてカスタムマイグレーションを実行 +""" +import os +import logging +from pathlib import Path +from sqlalchemy import text +from define_db.database import engine, SQLALCHEMY_DATABASE_URL +from define_db.models import Base + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +DB_PATH = Path("/data/sql_app.db") + +REQUIRED_TABLES = [ + 'users', 'projects', 'runs', 'processes', + 'operations', 'edges', 'ports', 'port_connections', + 'process_operations' +] + +# ============================================ +# カスタムマイグレーション定義 +# ============================================ +MIGRATIONS = [ + { + "version": "001", + "description": "Ensure storage_mode column in runs", + "check": "SELECT 1 FROM pragma_table_info('runs') WHERE name='storage_mode'", + "sql": "ALTER TABLE runs ADD COLUMN storage_mode VARCHAR(10)" + }, + { + "version": "002", + "description": "Ensure process_type column in processes", + "check": "SELECT 1 FROM pragma_table_info('processes') WHERE name='process_type'", + "sql": "ALTER TABLE processes ADD COLUMN process_type VARCHAR(256)" + }, + { + "version": "003", + "description": "Ensure display_visible column in runs", + "check": "SELECT 1 FROM pragma_table_info('runs') WHERE name='display_visible'", + "sql": "ALTER TABLE runs ADD COLUMN display_visible BOOLEAN DEFAULT 1 NOT NULL" + }, +] + + +def check_database_file() -> dict: + """データベースファイルの状態をチェック""" + result = { + 'exists': False, + 'size': 0, + 'is_empty': True, + 'is_readable': False + } + + if DB_PATH.exists(): + result['exists'] = True + result['size'] = DB_PATH.stat().st_size + result['is_empty'] = result['size'] == 0 + + try: + with open(DB_PATH, 'rb') as f: + f.read(16) + result['is_readable'] = True + except (IOError, PermissionError): + result['is_readable'] = False + + return result + + +def check_tables() -> dict: + """データベース内のテーブル存在をチェック""" + result = { + 'existing_tables': [], + 'missing_tables': [], + 'all_present': False + } + + try: + with engine.connect() as conn: + query = text("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in conn.execute(query)] + result['existing_tables'] = tables + result['missing_tables'] = [t for t in REQUIRED_TABLES if t not in tables] + result['all_present'] = len(result['missing_tables']) == 0 + except Exception as e: + logger.warning(f"テーブルチェック中にエラー: {e}") + result['missing_tables'] = REQUIRED_TABLES + + return result + + +def create_tables(): + """ + 全テーブルを作成 + + ⚠️ 重要: create_all()は既存テーブルのデータを削除しない + 既存テーブルはスキップされ、新規テーブルのみ作成される + """ + logger.info("テーブル作成を開始(既存テーブルはスキップ)...") + Base.metadata.create_all(engine) + logger.info("テーブル作成完了") + + +def run_custom_migrations(): + """ + カスタムマイグレーションを実行 + + 既存テーブルへのカラム追加など、create_all()で対応できない + スキーマ変更を実行する。既存データは保持される。 + """ + logger.info("カスタムマイグレーションをチェック...") + + with engine.connect() as conn: + applied_count = 0 + skipped_count = 0 + + for migration in MIGRATIONS: + version = migration["version"] + description = migration["description"] + + try: + result = conn.execute(text(migration["check"])) + if result.fetchone(): + logger.debug(f"Migration {version} already applied: {description}") + skipped_count += 1 + continue + except Exception: + skipped_count += 1 + continue + + logger.info(f"Applying migration {version}: {description}") + try: + conn.execute(text(migration["sql"])) + conn.commit() + logger.info(f"Migration {version} completed") + applied_count += 1 + except Exception as e: + logger.error(f"Migration {version} failed: {e}") + + if applied_count > 0: + logger.info(f"マイグレーション完了: {applied_count}件適用, {skipped_count}件スキップ") + else: + logger.info(f"マイグレーション: 全て適用済み ({skipped_count}件)") + + +def ensure_database_ready() -> dict: + """ + データベースが使用可能な状態であることを保証 + + Returns: + dict: 実行結果サマリー + """ + summary = { + 'action': None, + 'file_status': None, + 'table_status': None, + 'migrations_run': False, + 'success': False + } + + # Step 1: ファイル状態チェック + file_status = check_database_file() + summary['file_status'] = file_status + + need_create = False + + if not file_status['exists']: + logger.info(f"[DB Init] データベースファイルが存在しません: {DB_PATH}") + need_create = True + summary['action'] = 'create_new' + + elif file_status['is_empty']: + logger.warning(f"[DB Init] データベースファイルが空です (0 bytes)") + need_create = True + summary['action'] = 'initialize_empty' + + if need_create: + logger.info("[DB Init] テーブルを作成します...") + create_tables() + + table_status = check_tables() + summary['table_status'] = table_status + + if table_status['all_present']: + logger.info("[DB Init] 初期化完了") + summary['success'] = True + return summary + + # Step 2: テーブル存在チェック + table_status = check_tables() + summary['table_status'] = table_status + + if not table_status['all_present']: + missing = ', '.join(table_status['missing_tables']) + logger.info(f"[DB Init] 不足テーブル: {missing}") + logger.info("[DB Init] 不足テーブルを作成します(既存データは保持)...") + summary['action'] = 'create_missing' + + create_tables() + + table_status = check_tables() + summary['table_status'] = table_status + + # Step 3: カスタムマイグレーション + run_custom_migrations() + summary['migrations_run'] = True + + if summary['action'] is None: + summary['action'] = 'none' + + summary['success'] = True + logger.info(f"[DB Init] データベース準備完了 (テーブル数: {len(table_status['existing_tables'])})") + + return summary + + +if __name__ == "__main__": + result = ensure_database_ready() + print(f"\n=== 実行結果 ===") + print(f"アクション: {result['action']}") + print(f"成功: {result['success']}") + if result['table_status']: + print(f"テーブル数: {len(result['table_status']['existing_tables'])}") +``` + +### main.py への統合 + +```python +from fastapi import FastAPI +from contextlib import asynccontextmanager +import logging + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + FastAPIライフサイクル管理 + + 起動時: データベース初期化チェック + 終了時: リソースクリーンアップ + """ + # === 起動時処理 === + logger.info("=== FastAPI Starting ===") + + # DB初期化 + from init_db import ensure_database_ready + result = ensure_database_ready() + + if not result['success'] and result['action'] != 'none': + logger.error("データベース初期化に失敗しました") + + logger.info("=== FastAPI Ready ===") + + yield # アプリケーション実行 + + # === 終了時処理 === + logger.info("=== FastAPI Shutting Down ===") + + +# FastAPIアプリ作成(lifespanを指定) +app = FastAPI(lifespan=lifespan) +``` + +--- + +## 冪等性 + +すべての初期化処理は冪等性があり、何度実行しても安全です。 + +| 処理 | 動作 | +|------|------| +| `create_all()` | 既存テーブルはスキップ | +| カスタムマイグレーション | `check` クエリで適用済みを確認後スキップ | + +--- + +## 新しいマイグレーションの追加 + +### Step 1: MIGRATIONS リストに追加 + +```python +MIGRATIONS = [ + # ... 既存のマイグレーション ... + { + "version": "004", # 連番 + "description": "Add new_column to some_table", + "check": "SELECT 1 FROM pragma_table_info('some_table') WHERE name='new_column'", + "sql": "ALTER TABLE some_table ADD COLUMN new_column VARCHAR(256)" + }, +] +``` + +### Step 2: コンテナ再起動 + +```bash +docker compose restart log_server +``` + +### Step 3: ログ確認 + +```bash +docker logs labcode_log_server | grep "Migration" +``` + +--- + +## トラブルシューティング + +### DBファイルが0バイトになる + +**原因**: コンテナ異常終了、ディスク容量不足 + +**解決策**: +```bash +# バックアップから復元 +docker compose stop log_server +sudo cp data/sql_app.db.bak data/sql_app.db +sudo chown $(whoami):$(whoami) data/sql_app.db +docker compose start log_server +``` + +### マイグレーションが失敗する + +**原因**: SQLite固有の制限(カラム削除不可等) + +**解決策**: 複雑なスキーマ変更は個別スクリプト(`scripts/`)で対応 + +--- + +## Alembic不採用の理由 + +本プロジェクトではAlembicを採用しませんでした。 + +| 理由 | 説明 | +|------|------| +| 既存アプローチとの整合性 | `create_all()` + 個別スクリプト方式が既に実績あり | +| SQLite環境では過剰 | Alembicの強みは複数環境でのスキーマ同期 | +| 学習コスト | チーム全員がAlembicを習得する必要 | +| ロールバック需要の低さ | 開発環境ではDBリセットの方が早い | + +--- + +## 関連ドキュメント + +- [アップグレードガイド](./04_migration_guide.md) - 手動マイグレーションスクリプトの詳細 +- [システムアーキテクチャ](./01_architecture.md) - システム構成概要 + +--- + +## 作成日 + +2025-12-24 + +## 変更履歴 + +- v1.0: 初版 diff --git a/docs/07_remote_access.md b/docs/07_remote_access.md new file mode 100644 index 0000000..e9f97b1 --- /dev/null +++ b/docs/07_remote_access.md @@ -0,0 +1,137 @@ +# リモートアクセス設定 + +同一ネットワーク内の別のPCからLabCodeにアクセスする方法を説明します。 + +--- + +## 1. ホストPCのIPアドレスを確認 + +LabCodeが動作しているPCのIPアドレスを確認します。 + +### Linux + +```bash +ip addr +``` + +### Windows + +```bash +ipconfig +``` + +### macOS + +```bash +ifconfig +``` + +出力例(Linux): + +``` +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + inet 127.0.0.1/8 scope host lo +2: eth0: mtu 1500 + inet 192.168.1.5/24 brd 192.168.1.255 scope global dynamic eth0 +``` + +この例では、`192.168.1.5` がLAN内のIPアドレスです。 + +### IPアドレスの特徴 + +- `192.168.xxx.xxx`、`10.xxx.xxx.xxx`、`172.16.xxx.xxx` 〜 `172.31.xxx.xxx` の形式 +- `eth0`、`eno1`(有線LAN)または `wlan0`、`wlp2s0`(無線LAN)に関連 +- `scope global` と表示される + +--- + +## 2. クライアントPCのhostsファイルを編集 + +アクセスするPCのhostsファイルを編集して、ホスト名を登録します。 + +### Linux + +```bash +sudo nano /etc/hosts +``` + +以下の行を追加: + +``` +192.168.1.5 labcode-web-app.com +``` + +### Windows + +1. **管理者権限でメモ帳を開く**: + - スタートメニューで「メモ帳」を検索 + - 右クリック → 「管理者として実行」 + +2. **hostsファイルを開く**: + - `C:\Windows\System32\drivers\etc\hosts` を開く + +3. **以下の行を追加**: + ``` + 192.168.1.5 labcode-web-app.com + ``` + +### macOS + +```bash +sudo nano /etc/hosts +``` + +以下の行を追加: + +``` +192.168.1.5 labcode-web-app.com +``` + +**注意**: `192.168.1.5` は実際のIPアドレスに置き換えてください。 + +--- + +## 3. アクセス + +ブラウザで以下のURLにアクセス: + +| サービス | URL | +|---------|-----| +| Web UI | http://labcode-web-app.com:5173 | +| Admin Panel | http://labcode-web-app.com:5173/admin | +| API (Swagger UI) | http://labcode-web-app.com:8000/docs | + +--- + +## トラブルシューティング + +### アクセスできない場合 + +**1. ファイアウォールを確認** + +```bash +# Linux (UFW) +sudo ufw status +sudo ufw allow 5173 +sudo ufw allow 8000 + +# Windows +# コントロールパネル → Windows Defender ファイアウォール → 詳細設定 +# 受信の規則 → 新しい規則 → ポート → 5173, 8000 を許可 +``` + +**2. Dockerの設定を確認** + +```bash +docker compose ps +``` + +コンテナが正常に動作していることを確認してください。 + +**3. ネットワーク接続を確認** + +```bash +ping 192.168.1.5 +``` + +ホストPCに到達できることを確認してください。 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1734a11 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,128 @@ +# LabCode ドキュメント + +## 概要 + +LabCodeは、自動実験プロトコル言語プラットフォームです。このドキュメントでは、セットアップ、機能仕様、および運用ガイドを提供します。 + +## ドキュメント一覧 + +### 導入ガイド + +| ドキュメント | 説明 | +|-------------|------| +| [00_quickstart.md](./00_quickstart.md) | **クイックスタートガイド** - 最初にお読みください | + +### 仕様書 + +| ドキュメント | 説明 | +|-------------|------| +| [01_architecture.md](./01_architecture.md) | システムアーキテクチャ概要 | +| [02_admin_panel.md](./02_admin_panel.md) | Admin Panel 機能仕様 | +| [03_api_reference.md](./03_api_reference.md) | API リファレンス | + +### セットアップガイド + +| ドキュメント | 説明 | +|-------------|------| +| [04_migration_guide.md](./04_migration_guide.md) | アップグレード手順 | +| [05_s3_storage_setup.md](./05_s3_storage_setup.md) | S3ストレージ設定手順 | +| [06_db_auto_initialization.md](./06_db_auto_initialization.md) | DB自動初期化機能 | +| [07_remote_access.md](./07_remote_access.md) | リモートアクセス設定 | + +## クイックスタート + +### 1. 環境構築 + +```bash +# リポジトリをクローン +git clone --recurse-submodules + +# サブモジュールを更新 +git submodule update --init --recursive + +# Docker Compose で起動 +docker compose up -d +``` + +### 2. アクセス + +- **Web UI**: http://localhost:5173 +- **Admin Panel**: http://localhost:5173/admin +- **API**: http://localhost:8000 + +### 3. 機能確認 + +1. Run Listページにログイン +2. ヘッダーの「Admin」バッジをクリック +3. Admin Dashboardが表示されることを確認 + +## 新機能サマリー + +### S3ストレージ連携 + +- **Storage v2 API**: S3/ローカル統合ストレージ +- **storage_mode**: Run毎にS3/ローカルを自動判定 +- **ファイルブラウザ**: S3上のファイルを直接閲覧・ダウンロード + +### Admin Panel + +- **ユーザー管理**: ユーザーの作成・削除 +- **プロジェクト管理**: プロジェクトの作成・編集・削除 +- **実験実行**: 4ステップウィザードで実験を実行 +- **ダッシュボード**: 統計情報とクイックアクション + +### その他の改善 + +- **Forbidden ページ**: 詳細説明とナビゲーション追加 +- **ナビゲーション**: Admin ↔ Run List 相互リンク +- **Feature Flag**: 機能の有効/無効を環境変数で制御 + +## アーキテクチャ概要 + +```mermaid +graph TB + subgraph Docker["labcode-test-environment"] + subgraph Frontend["Web UI"] + WebApp["labcode-web-app
React + Vite
Port: 5173"] + end + + subgraph Backend["Backend API"] + LogServer["labcode-log-server
FastAPI + SQLAlchemy
Port: 8000"] + end + + subgraph Simulator["Simulator"] + Sim["labcode-sim
Port: 8888"] + end + + subgraph Storage["Storage"] + DB[(SQLite
Database)] + S3[(AWS S3
Optional)] + end + end + + WebApp <--> LogServer + LogServer <--> Sim + LogServer --> DB + LogServer --> S3 + Sim --> S3 +``` + +## 推奨読み順 + +1. **初めての方**: [クイックスタート](./00_quickstart.md) → [システムアーキテクチャ](./01_architecture.md) → [Admin Panel仕様](./02_admin_panel.md) +2. **API利用者**: [APIリファレンス](./03_api_reference.md) +3. **S3ストレージ導入**: [S3ストレージ設定](./05_s3_storage_setup.md) +4. **既存環境からのアップグレード**: [アップグレードガイド](./04_migration_guide.md) +5. **DB自動初期化を理解したい方**: [DB自動初期化ガイド](./06_db_auto_initialization.md) +6. **他のPCからアクセスしたい方**: [リモートアクセス設定](./07_remote_access.md) + +## アップグレードシナリオ + +お使いの環境に応じて、適切なパスを選択してください: + +| シナリオ | 説明 | マイグレーション | +|---------|------|-----------------| +| A | S3導入前 → 最新版 | 必要 | +| B | S3導入後 → 最新版(Admin Panel追加) | 不要 | + +詳細は[アップグレードガイド](./04_migration_guide.md)を参照してください。 diff --git a/labcode-log-server b/labcode-log-server new file mode 160000 index 0000000..c4dd9f9 --- /dev/null +++ b/labcode-log-server @@ -0,0 +1 @@ +Subproject commit c4dd9f91b84400a9b453f0e65ae7861839ade67e diff --git a/labcode-sim b/labcode-sim new file mode 160000 index 0000000..534794f --- /dev/null +++ b/labcode-sim @@ -0,0 +1 @@ +Subproject commit 534794fef1615245c897b0ff0f8a74df8d6593b4 diff --git a/labcode-web-app b/labcode-web-app new file mode 160000 index 0000000..ecc5426 --- /dev/null +++ b/labcode-web-app @@ -0,0 +1 @@ +Subproject commit ecc5426acfc074404073f9f2e69a4f1cc1151fe4 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..099edfb --- /dev/null +++ b/tests/README.md @@ -0,0 +1,98 @@ +# Integration Tests + +This directory contains integration tests for the LabCode system, specifically testing the communication between `lab_simulator` and `log_server`. + +## Purpose + +These tests verify that: +1. API endpoints exist at the correct paths (with `/api` prefix) +2. Endpoints respond with expected status codes +3. lab_simulator can successfully communicate with log_server + +## Prerequisites + +1. **Docker containers must be running**: + ```bash + cd /home/ayumu/Documents/Science-Aid/SciAid-LabCode/labcode-test-environment + docker compose up -d + ``` + +2. **Install pytest** (if not already installed): + ```bash + pip install pytest requests + ``` + +3. **Verify log_server is accessible**: + ```bash + curl http://localhost:8000/docs + # Should return Swagger UI HTML + ``` + +## Running the Tests + +### Run all tests +```bash +cd /home/ayumu/Documents/Science-Aid/SciAid-LabCode/labcode-test-environment +python -m pytest tests/test_integration.py -v +``` + +### Run specific test class +```bash +# Test only runs endpoint +python -m pytest tests/test_integration.py::TestRunsEndpoint -v + +# Test only operations endpoint +python -m pytest tests/test_integration.py::TestOperationsEndpoint -v +``` + +### Run specific test +```bash +python -m pytest tests/test_integration.py::TestRunsEndpoint::test_runs_endpoint_exists -v +``` + +## Expected Results + +All tests should **PASS** if: +- Docker containers are running +- log_server is accessible at `http://localhost:8000` +- All endpoints are registered with `/api` prefix + +## Test Structure + +### TestRunsEndpoint +- `test_runs_endpoint_exists`: Verifies `/api/runs/` accepts POST requests +- `test_runs_endpoint_404_without_api_prefix`: Verifies `/runs/` (without `/api`) returns 404 + +### TestOperationsEndpoint +- `test_operations_endpoint_exists`: Verifies `/api/operations/` accepts POST requests +- `test_operations_endpoint_404_without_api_prefix`: Verifies `/operations/` returns 404 + +### TestProcessesEndpoint +- `test_processes_endpoint_exists`: Verifies `/api/processes/` accepts POST requests +- `test_processes_endpoint_404_without_api_prefix`: Verifies `/processes/` returns 404 + +### TestEdgesEndpoint +- `test_edges_endpoint_exists`: Verifies `/api/edges/` accepts POST requests +- `test_edges_endpoint_404_without_api_prefix`: Verifies `/edges/` returns 404 + +### TestPatchEndpoints +- Tests for PATCH endpoints on runs, operations, and processes + +## Troubleshooting + +### Tests fail with "Connection refused" +- **Cause**: Docker containers are not running +- **Solution**: Run `docker compose up -d` + +### Tests fail with 404 for `/api/runs/` +- **Cause**: log_server endpoints are not registered with `/api` prefix +- **Solution**: Check `labcode-log-server/app/main.py` router registration + +### Tests fail with 422 (Validation Error) +- **Expected behavior**: This is normal for tests that send incomplete data +- The test passes as long as it's not 404 + +## Related Documentation + +- [debug_v02.md](../playground_impl/history/debug_v02.md): Debugging report for the 404 error issue +- [README.md](../README.md): Main setup instructions for labcode-test-environment diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..7be67c0 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,211 @@ +""" +Integration tests for lab_simulator <-> log_server API communication + +These tests verify that: +1. API endpoints exist at the correct paths +2. Endpoints respond with expected status codes +3. Response structures contain required fields + +Run these tests to ensure lab_simulator can successfully communicate with log_server. +""" + +import requests +import pytest + + +LOG_SERVER_URL = "http://localhost:8000" + + +class TestRunsEndpoint: + """Test /api/runs/ endpoint""" + + def test_runs_endpoint_exists(self): + """Test that /api/runs/ endpoint exists and accepts POST requests""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/runs/', + data={ + "project_id": 1, + "file_name": "test_protocol", + "checksum": "test_checksum_123", + "user_id": 1, + "storage_address": "/test/storage" + } + ) + # Should return 200 or 422 (validation error), but not 404 + assert response.status_code in [200, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_runs_endpoint_404_without_api_prefix(self): + """Test that /runs/ (without /api prefix) returns 404""" + response = requests.post( + url=f'{LOG_SERVER_URL}/runs/', + data={ + "project_id": 1, + "file_name": "test_protocol", + "checksum": "test_checksum_123", + "user_id": 1, + "storage_address": "/test/storage" + } + ) + # Should return 404 because /runs/ doesn't exist (only /api/runs/ does) + assert response.status_code == 404, \ + f"Expected 404 for /runs/ without /api prefix, got {response.status_code}" + + +class TestOperationsEndpoint: + """Test /api/operations/ endpoint""" + + def test_operations_endpoint_exists(self): + """Test that /api/operations/ endpoint exists""" + # First create a run and process to get valid process_id + # For now, just test that the endpoint exists + response = requests.post( + url=f'{LOG_SERVER_URL}/api/operations/', + data={ + "process_id": 1, + "name": "test_operation", + "status": "not started", + "storage_address": "/test/storage", + "is_transport": False, + "is_data": False + } + ) + # Should return 200 or 422 (validation error), but not 404 + assert response.status_code in [200, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_operations_endpoint_404_without_api_prefix(self): + """Test that /operations/ (without /api prefix) returns 404""" + response = requests.post( + url=f'{LOG_SERVER_URL}/operations/', + data={ + "process_id": 1, + "name": "test_operation", + "status": "not started", + "storage_address": "/test/storage", + "is_transport": False, + "is_data": False + } + ) + assert response.status_code == 404, \ + f"Expected 404 for /operations/ without /api prefix, got {response.status_code}" + + +class TestProcessesEndpoint: + """Test /api/processes/ endpoint""" + + def test_processes_endpoint_exists(self): + """Test that /api/processes/ endpoint exists""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/processes/', + data={ + "name": "test_process", + "run_id": 1, + "storage_address": "/test/storage" + } + ) + # Should return 200 or 422 (validation error), but not 404 + assert response.status_code in [200, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_processes_endpoint_404_without_api_prefix(self): + """Test that /processes/ (without /api prefix) returns 404""" + response = requests.post( + url=f'{LOG_SERVER_URL}/processes/', + data={ + "name": "test_process", + "run_id": 1, + "storage_address": "/test/storage" + } + ) + assert response.status_code == 404, \ + f"Expected 404 for /processes/ without /api prefix, got {response.status_code}" + + +class TestEdgesEndpoint: + """Test /api/edges/ endpoint""" + + def test_edges_endpoint_exists(self): + """Test that /api/edges/ endpoint exists""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/edges/', + data={ + "run_id": 1, + "from_id": 1, + "to_id": 2 + } + ) + # Should return 200 or 422 (validation error), but not 404 + assert response.status_code in [200, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_edges_endpoint_404_without_api_prefix(self): + """Test that /edges/ (without /api prefix) returns 404""" + response = requests.post( + url=f'{LOG_SERVER_URL}/edges/', + data={ + "run_id": 1, + "from_id": 1, + "to_id": 2 + } + ) + assert response.status_code == 404, \ + f"Expected 404 for /edges/ without /api prefix, got {response.status_code}" + + +class TestEndpointResponses: + """Test response structures""" + + def test_successful_response_contains_id(self): + """Test that successful POST responses contain 'id' field""" + # This test requires valid data, so it may fail if database constraints are strict + # It's mainly to document the expected response structure + pass # TODO: Implement with valid test data + + +class TestPatchEndpoints: + """Test PATCH endpoints""" + + def test_runs_patch_endpoint_exists(self): + """Test that /api/runs/{id} PATCH endpoint exists""" + response = requests.patch( + url=f'{LOG_SERVER_URL}/api/runs/1', + data={ + "attribute": "status", + "new_value": "running" + } + ) + # Should return 200, 404 (run not found), or 422, but not 405 (method not allowed) + assert response.status_code in [200, 404, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_operations_patch_endpoint_exists(self): + """Test that /api/operations/{id} PATCH endpoint exists""" + response = requests.patch( + url=f'{LOG_SERVER_URL}/api/operations/1', + data={ + "attribute": "status", + "new_value": "running" + } + ) + # Should return 200, 404, or 422, but not 405 + assert response.status_code in [200, 404, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_processes_patch_endpoint_exists(self): + """Test that /api/processes/{id} PATCH endpoint exists""" + response = requests.patch( + url=f'{LOG_SERVER_URL}/api/processes/1', + data={ + "attribute": "storage_address", + "new_value": "/new/storage" + } + ) + # Should return 200, 404, or 422, but not 405 + assert response.status_code in [200, 404, 422], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + +if __name__ == "__main__": + # Run tests with pytest + pytest.main([__file__, "-v"]) diff --git a/tests/test_storage_v2.py b/tests/test_storage_v2.py new file mode 100644 index 0000000..42fbf01 --- /dev/null +++ b/tests/test_storage_v2.py @@ -0,0 +1,151 @@ +""" +Storage V2 API tests for HAL-based storage operations + +These tests verify: +1. Metadata dump endpoint (all storage modes) +2. HAL-based batch download endpoint +3. Storage info endpoint +4. Batch download estimate endpoint + +Run these tests with: pytest tests/test_storage_v2.py -v +""" + +import requests +import pytest +import json + +LOG_SERVER_URL = "http://localhost:8000" + + +class TestMetadataDumpEndpoint: + """Test /api/v2/storage/dump/{run_id} endpoint""" + + def test_dump_endpoint_exists(self): + """Test 1: Metadata dump endpoint exists and returns 404 for non-existent run""" + response = requests.get( + url=f'{LOG_SERVER_URL}/api/v2/storage/dump/99999' + ) + # Should return 404 (run not found), not 405 (method not allowed) + assert response.status_code == 404, \ + f"Expected 404 for non-existent run, got {response.status_code}" + + def test_dump_for_existing_run(self): + """Test 2: Metadata dump works for existing run (any storage mode)""" + # Try to get dump for run ID 1 (should exist in test environment) + response = requests.get( + url=f'{LOG_SERVER_URL}/api/v2/storage/dump/1' + ) + # Should return 200 (success) or 404 (run not found), not 400 (mode restriction) + assert response.status_code in [200, 404], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + if response.status_code == 200: + # Verify it's a SQLite file + assert response.headers.get('content-type') == 'application/x-sqlite3', \ + f"Expected SQLite content type, got {response.headers.get('content-type')}" + + +class TestBatchDownloadV2Endpoint: + """Test /api/v2/storage/batch-download endpoint""" + + def test_batch_download_endpoint_exists(self): + """Test 3: HAL batch download endpoint exists""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/v2/storage/batch-download', + json={"run_ids": [99999]} + ) + # Should return 404 (no runs found) or 200, not 405 (method not allowed) + assert response.status_code in [200, 404], \ + f"Unexpected status code: {response.status_code}. Response: {response.text}" + + def test_batch_download_empty_request(self): + """Test 4: Batch download rejects empty run_ids""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/v2/storage/batch-download', + json={"run_ids": []} + ) + # Should return 400 (bad request) or 422 (validation error) + assert response.status_code in [400, 422], \ + f"Expected 400/422 for empty run_ids, got {response.status_code}" + + def test_batch_download_returns_zip(self): + """Test 5: Batch download returns ZIP for existing runs""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/v2/storage/batch-download', + json={"run_ids": [1]} + ) + if response.status_code == 200: + # Verify it's a ZIP file + assert response.headers.get('content-type') == 'application/zip', \ + f"Expected ZIP content type, got {response.headers.get('content-type')}" + + +class TestBatchDownloadEstimateV2: + """Test /api/v2/storage/batch-download/estimate endpoint""" + + def test_estimate_endpoint_exists(self): + """Test 6: Batch download estimate endpoint exists""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/v2/storage/batch-download/estimate', + json={"run_ids": [1]} + ) + # Should return 200 or handle gracefully + assert response.status_code in [200, 404], \ + f"Unexpected status code: {response.status_code}" + + def test_estimate_response_structure(self): + """Test 7: Estimate response has correct structure""" + response = requests.post( + url=f'{LOG_SERVER_URL}/api/v2/storage/batch-download/estimate', + json={"run_ids": [1, 2, 3]} + ) + if response.status_code == 200: + data = response.json() + # Verify required fields + assert 'run_count' in data, "Missing run_count field" + assert 'total_files' in data, "Missing total_files field" + assert 'estimated_size_mb' in data, "Missing estimated_size_mb field" + assert 'can_download' in data, "Missing can_download field" + assert 'runs_detail' in data, "Missing runs_detail field" + + +class TestStorageInfoV2: + """Test /api/v2/storage/info/{run_id} endpoint""" + + def test_storage_info_endpoint_exists(self): + """Test 8: Storage info endpoint exists""" + response = requests.get( + url=f'{LOG_SERVER_URL}/api/v2/storage/info/1' + ) + # Should return 200 or 404 + assert response.status_code in [200, 404], \ + f"Unexpected status code: {response.status_code}" + + def test_storage_info_response_structure(self): + """Test 9: Storage info response has correct structure""" + response = requests.get( + url=f'{LOG_SERVER_URL}/api/v2/storage/info/1' + ) + if response.status_code == 200: + data = response.json() + # Verify required fields + assert 'mode' in data, "Missing mode field" + assert 'storage_address' in data, "Missing storage_address field" + assert 'full_path' in data, "Missing full_path field" + + +class TestStorageListV2: + """Test /api/v2/storage/list/{run_id} endpoint""" + + def test_list_endpoint_exists(self): + """Test 10: Storage list endpoint exists""" + response = requests.get( + url=f'{LOG_SERVER_URL}/api/v2/storage/list/1' + ) + # Should return 200 or 404 + assert response.status_code in [200, 404], \ + f"Unexpected status code: {response.status_code}" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])