Compare commits
3 Commits
aded5562cd
...
da05455e7c
| Author | SHA1 | Date |
|---|---|---|
|
|
da05455e7c | |
|
|
44f88ff04b | |
|
|
10f772942a |
|
|
@ -1,145 +1,85 @@
|
||||||
# 📋 Ponshu Room Lite - Current TODO List
|
# 📋 Ponshu Room Lite - Current TODO List & Roadmap
|
||||||
|
|
||||||
**Last Updated**: 2026-01-21
|
**Last Updated**: 2026-02-21 (v1.0.16)
|
||||||
**Status**: Active Task Tracking
|
**Status**: Active Task Tracking (Based on Claude's Comprehensive Review)
|
||||||
**For**: Multi-AI Collaboration (Claude + Antigravity + Gemini)
|
**For**: Multi-AI Collaboration (Claude + Antigravity + Gemini)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔴 CRITICAL: Deferred Tasks (DO NOT FORGET)
|
## 🔴 High Priority (品質・配布に直結 / 即時対応推奨)
|
||||||
|
|
||||||
### Dark Mode Visibility Fixes (Phase 1 - INCOMPLETE)
|
- [ ] **H1. ダークモード完全対応** (推定: 6-8時間)
|
||||||
|
- **対象**: `sake_detail_screen.dart:794` - 9箇所の `Theme.of(context).primaryColor` を `colorScheme` に置換。コアスクリーンでの視認性問題のため優先。
|
||||||
- [x] ✅ Create Dark Mode guidelines ([DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md))
|
- [ ] **H2. 大規模ファイルのリファクタリング** (推定: 8-12時間)
|
||||||
- [x] ✅ Redesign app_theme.dart to use Material 3 ColorScheme
|
- `camera_screen.dart` (966行): `CameraControls`, `PhotoPreview`, `GalleryPicker` に分割。
|
||||||
- [x] ✅ Fix guide_screen.dart (section headers to secondary color)
|
- `sake_detail_screen.dart` (794行): 分割済みだが監視継続 (`BasicInfo`, `TasteChart`, `Sakenowa`, `Pricing`)。
|
||||||
- [x] ✅ Fix soul_screen.dart (10+ manual dark mode checks removed)
|
- `menu_pricing_screen.dart` (650行): `PricingInputSection`, `PricingPreview` に分割。
|
||||||
- [ ] ⏸️ **DEFERRED**: Fix [sake_detail_screen.dart](../lib/screens/sake_detail_screen.dart)
|
- [ ] **H3. Synology Proxy永続化対応** (推定: 4-6時間)
|
||||||
- **Why Deferred**: 1500+ lines, recently modified, needs careful review
|
- **現状**: In-Memoryのため、再起動で利用回数クォータがリセットされる。
|
||||||
- **Issues**: 9 instances of `Theme.of(context).primaryColor` usage
|
- **解決策**: Redis導入(推奨)またはファイルベースの永続化。
|
||||||
- **Location**: Lines need investigation with `Grep` tool
|
|
||||||
- **Priority**: High (core feature screen)
|
|
||||||
- **Estimated Time**: 2-3 hours
|
|
||||||
- **Action Plan**:
|
|
||||||
1. Search for all `primaryColor` instances in file
|
|
||||||
2. Replace with appropriate `colorScheme` properties
|
|
||||||
3. Test thoroughly in both Light and Dark modes
|
|
||||||
4. Verify all UI elements (buttons, icons, charts, dialogs)
|
|
||||||
|
|
||||||
- [ ] ⏸️ **DEFERRED**: Fix shop_settings_screen.dart
|
|
||||||
- **Issues**: Manual dark mode checks present
|
|
||||||
- **Priority**: Medium
|
|
||||||
- **Estimated Time**: 1 hour
|
|
||||||
|
|
||||||
- [ ] ⏸️ **DEFERRED**: Fix remaining widgets with `primaryColor` usage
|
|
||||||
- **Files**: badge_case.dart, level_title_card.dart, sake_grid_item.dart, etc.
|
|
||||||
- **Count**: ~40 instances across multiple files
|
|
||||||
- **Priority**: Low-Medium
|
|
||||||
- **Estimated Time**: 4-6 hours total
|
|
||||||
- **Strategy**: Fix incrementally during feature work
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟡 Phase 2: Planned Features (READY TO START)
|
## 🟡 Medium Priority (UX改善・機能追加)
|
||||||
|
|
||||||
See detailed implementation plan: [PHASE_2_IMPLEMENTATION_PLAN.md](PHASE_2_IMPLEMENTATION_PLAN.md)
|
- [ ] **M1. AI「あわせて飲みたい」機能実装** (推定: 12-16時間)
|
||||||
|
- **要件**: 履歴分析、未登録銘柄からの推薦文生成。ソムリエタブのプレースホルダーを実稼働させる。
|
||||||
**Summary**:
|
- [ ] **M2. チャート手動編集UIの完成** (推定: 8-10時間)
|
||||||
1. **Help Button Placement** (Pattern C: Hybrid approach) - 6 hours
|
- **内容**: AI生成の5軸チャートをユーザーが微調整できる機能。
|
||||||
2. **AI-Powered "あわせて飲みたい" Recommendations** - 12 hours
|
- [ ] **M3. ヘルプボタンの配置改善** (推定: 6時間)
|
||||||
3. **AI Analysis Info Editing** - 8 hours
|
- **方針**: Pattern C (Hybrid approach) - 各画面の状況に応じて配置。
|
||||||
|
- [ ] **M4. Pro版機能の実装** (推定: 20-24時間)
|
||||||
**Total Estimated Time**: 26 hours
|
- **要件**: QRスキャン連携、Instagram投稿生成機能、売上アナリティクスダッシュボード等。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟢 Phase 3: Technical Debt & Cleanup
|
## 🟢 Low Priority (技術的負債・長期改善)
|
||||||
|
|
||||||
### Coach Mark / Tutorial Cleanup
|
- [ ] **L1. debugPrint整理** (推定: 3-4時間)
|
||||||
- [ ] Remove `hasSeenTutorial` from UserProfile model
|
- **対象**: 221箇所(23ファイル)。`if (kDebugMode)` で囲む(主に `gemini_service.dart`, `analysis_cache_service.dart`)。
|
||||||
- [ ] Remove `hasSeenCoachMarks` from UserProfile model
|
- [ ] **L2. ハードコード色の段階的修正** (推定: 10-15時間)
|
||||||
- [ ] Remove tutorial-related methods from theme_provider.dart
|
- **対象**: 意図的な例外(カメラビューなど)を除くハードコード色。`onboarding_dialog.dart` や `pending_analysis_screen.dart` などから優先。
|
||||||
- [ ] Remove tutorial-related images/assets
|
- [ ] **L3. 真の画像圧縮実装** (推定: 3-4時間)
|
||||||
- **Why**: Tutorial service was deleted due to persistence bugs
|
- **現状**: コピーのみの `ImageCompressionService` を `flutter_image_compress` などの圧縮処理に置き換え。
|
||||||
- **Priority**: Low (not breaking anything, just cluttering code)
|
- [ ] **L4. Tutorial/CoachMark残存コードの削除** (推定: 1-2時間)
|
||||||
- **Estimated Time**: 1 hour
|
- **対象**: `UserProfile` に存在したチュートリアル系不要フィールドと関連アセットの削除の完全完了。
|
||||||
|
- [ ] **L5. テストコード導入** (推定: 8-12時間)
|
||||||
### Flutter Analyze Warnings
|
- `SakeItem` JSON変換、`LevelCalculator`、`GeminiService` 等々のカバレッジ向上。
|
||||||
- [ ] Fix 49 warnings (see `flutter analyze` output)
|
- [ ] **L6. UI/UXのさらなる改善**
|
||||||
- Unused imports
|
- バッジアンロックモーダル、スキャン時のアニメーション、タブレット対応(レスポンシブ)等。
|
||||||
- Deprecated API usage
|
|
||||||
- Type warnings
|
|
||||||
- **Priority**: Medium
|
|
||||||
- **Estimated Time**: 2 hours
|
|
||||||
|
|
||||||
### Image Compression
|
|
||||||
- [ ] Implement real image compression (not just file copy)
|
|
||||||
- **Current Issue**: ImageCompressionService just copies files
|
|
||||||
- **Solution**: Use flutter_image_compress or similar
|
|
||||||
- **Priority**: Medium (affects storage usage)
|
|
||||||
- **Estimated Time**: 3 hours
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Documentation Status
|
## 🔮 Future / 長期構想 (Phase 3+)
|
||||||
|
|
||||||
### ✅ Complete & Up-to-date
|
- [ ] **F1. Firebase同期** (16-20時間): クラウドバックアップ、複数デバイス同期。
|
||||||
- [DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md) - Dark Mode implementation guide
|
- [ ] **F2. Posimai Core Platform構築** (40時間〜): 日本酒・香道アプリの共通基盤化。
|
||||||
- [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md) - High-level roadmap
|
- [ ] **F3. 多言語対応** (20時間): 英語・中国語対応(ARBファイル利用)。
|
||||||
- [UI_UX_BACKLOG.md](UI_UX_BACKLOG.md) - UI/UX improvement tracking
|
- [ ] **F4. 酒蔵マップ実データ統合** (12時間): プレースホルダーから実データへの連携、GPS連動等。
|
||||||
- [CURSOR_CHAT_MASTER_CONTEXT.md](architecture/CURSOR_CHAT_MASTER_CONTEXT.md) - Antigravity handoff
|
|
||||||
|
|
||||||
### 🔄 Needs Update
|
|
||||||
- [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md) - Mark Dark Mode tasks as partially complete
|
|
||||||
- [UI_UX_BACKLOG.md](UI_UX_BACKLOG.md) - Update Coach Mark status
|
|
||||||
|
|
||||||
### 📝 New Documents Created
|
|
||||||
- This file (PROJECT_TODO.md)
|
|
||||||
- [README.md](README.md) - Documentation index (TO BE CREATED)
|
|
||||||
- [PHASE_2_IMPLEMENTATION_PLAN.md](PHASE_2_IMPLEMENTATION_PLAN.md) (TO BE CREATED)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Recommended Next Steps
|
## 🏗️ Synology環境の堅牢化 (運用サーバー対応)
|
||||||
|
|
||||||
For immediate work, choose ONE of the following:
|
- [ ] **S1. Proxy Serverのレート制限永続化** (Critical / =H3)
|
||||||
|
- Container Managerで Redis コンテナを立ち上げ、Node.jsプロキシに接続して利用履歴を永続化。
|
||||||
### Option A: Complete Dark Mode Fixes
|
- [ ] **S2. バックアップ戦略の確立** (Critical)
|
||||||
1. Fix sake_detail_screen.dart (~2-3 hours)
|
- Hyper Backupでのプロジェクト全体の外部保存、Dokploy / Giteaの定期ボリュームバックアップの設定。
|
||||||
2. Fix shop_settings_screen.dart (~1 hour)
|
- [ ] **S3. SSL/TLS対応**
|
||||||
3. Mark Dark Mode project as complete
|
- Tailscale Funnel や Synology Application Portal (リバースプロキシ) の構築。
|
||||||
|
- [ ] **S4. 監視・アラート設定**
|
||||||
### Option B: Start Phase 2 Features
|
- Uptime Kuma などを導入し、ダウン時に通知を飛ばす体制づくり。
|
||||||
1. Implement Help Button Placement (~6 hours)
|
- [ ] **S5-10. インフラのさらなる拡張** (Important/Nice to Have)
|
||||||
2. Creates user value immediately
|
- CI/CD構築(Gitea Actions, Dokploy連携等)、共有基盤化、Webhook通知など。
|
||||||
3. Defer remaining Dark Mode fixes to incremental work
|
|
||||||
|
|
||||||
### Option C: Quick Wins
|
|
||||||
1. Technical debt cleanup (~3 hours total)
|
|
||||||
- Coach mark removal
|
|
||||||
- Flutter analyze fixes
|
|
||||||
2. Builds confidence, cleans codebase
|
|
||||||
|
|
||||||
**Recommended by Previous Claude Instance**: Option B (Phase 2) - Delivers user value while Dark Mode is "good enough" for now.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚨 Known Issues to Avoid
|
## 📅 推奨作業順序 (今後2週間)
|
||||||
|
|
||||||
1. **Image Path Repair Service** - Works correctly after Day 4 bug fix
|
1. **品質安定化 (Week 1)**
|
||||||
2. **Backup/Restore** - Functional, tested successfully
|
- Day 1-2: Synology Proxy永続化 (S1 / H3)
|
||||||
3. **Tutorial Service** - Deleted, do not attempt to restore
|
- Day 3-4: ダークモード完全対応 (H1)
|
||||||
4. **Dark Mode** - Fixed for core screens, remaining issues documented above
|
- Day 5: バックアップ戦略確立 (S2)
|
||||||
|
2. **機能拡張 or インフラ強化 (Week 2)**
|
||||||
---
|
- *Option A (機能優先)*: 推薦機能(M1) & チャート微調整(M2)
|
||||||
|
- *Option B (インフラ優先)*: Dokploy設定 & CI/CDテスト & 監視設定
|
||||||
## 📞 For Future AI Collaborators
|
|
||||||
|
|
||||||
- **Before modifying Dark Mode code**: Read [DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md)
|
|
||||||
- **Before adding new features**: Check this TODO and [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md)
|
|
||||||
- **Before committing**: Run `flutter analyze` and fix new warnings
|
|
||||||
- **For documentation questions**: See [docs/README.md](README.md) (index file)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This is a living document. Update status after completing tasks.*
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,11 @@ class Secrets {
|
||||||
|
|
||||||
/// AI Mode: Proxy(Home) vs Direct(Cloud)
|
/// AI Mode: Proxy(Home) vs Direct(Cloud)
|
||||||
/// If false, connects directly to Google Gemini API (Works anywhere).
|
/// If false, connects directly to Google Gemini API (Works anywhere).
|
||||||
/// Release build: --dart-define=USE_PROXY=true
|
/// Development (with Tailscale): --dart-define=USE_PROXY=true
|
||||||
|
/// General distribution: false (each user provides their own Gemini API key)
|
||||||
static const bool useProxy = bool.fromEnvironment(
|
static const bool useProxy = bool.fromEnvironment(
|
||||||
'USE_PROXY',
|
'USE_PROXY',
|
||||||
defaultValue: false,
|
defaultValue: false, // ← 一般配布用: Direct API(各自がAPIキー設定)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Gemini API Key
|
||||||
|
GEMINI_API_KEY=your_gemini_api_key_here
|
||||||
|
|
||||||
|
# Proxy Authentication Token (recommended for security)
|
||||||
|
# Generate a random string: openssl rand -hex 32
|
||||||
|
PROXY_AUTH_TOKEN=your_secure_random_token_here
|
||||||
|
|
||||||
|
# Daily request limit per device
|
||||||
|
DAILY_LIMIT=50
|
||||||
|
|
||||||
|
# Redis connection settings (default values for Docker Compose)
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Environment variables (contains secrets)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
data/
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
# Ponshu Room Proxy Server (Redis永続化対応)
|
||||||
|
|
||||||
|
Gemini APIキーを隠蔽し、デバイスごとの利用制限(レート制限)を管理するプロキシサーバー。
|
||||||
|
|
||||||
|
## 🎯 機能
|
||||||
|
|
||||||
|
- **APIキー保護**: クライアントアプリにAPIキーを含めず、プロキシサーバーのみが保持
|
||||||
|
- **レート制限**: デバイスIDごとに1日あたりのリクエスト数を制限
|
||||||
|
- **Redis永続化**: Dockerコンテナ再起動後もカウントを維持
|
||||||
|
- **認証**: Bearer Token認証(オプション)
|
||||||
|
- **Dokploy対応**: 将来のDokploy移行に対応した構成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 クイックスタート(Synology NAS)
|
||||||
|
|
||||||
|
### 1. 環境変数ファイル作成
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env` を編集してAPIキーと認証トークンを設定:
|
||||||
|
|
||||||
|
```env
|
||||||
|
GEMINI_API_KEY=AIzaSy...(あなたのAPIキー)
|
||||||
|
PROXY_AUTH_TOKEN=$(openssl rand -hex 32) # ランダム生成推奨
|
||||||
|
DAILY_LIMIT=50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Composeで起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 動作確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
# 期待される出力: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Redis データ確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redisコンテナに接続
|
||||||
|
docker exec -it ponshu-redis redis-cli
|
||||||
|
|
||||||
|
# 使用状況確認
|
||||||
|
KEYS usage:*
|
||||||
|
GET "usage:device-123:2026-02-21"
|
||||||
|
TTL "usage:device-123:2026-02-21"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 運用コマンド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ログ確認
|
||||||
|
docker-compose logs -f proxy
|
||||||
|
|
||||||
|
# 再起動
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# 停止
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# 完全削除(データも削除)
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 ドキュメント
|
||||||
|
|
||||||
|
- **移行ガイド**: [REDIS_MIGRATION_GUIDE.md](REDIS_MIGRATION_GUIDE.md) - In-MemoryからRedis永続化への移行手順
|
||||||
|
- **セットアップガイド**: [../../docs/SYNOLOGY_PROXY_SETUP.md](../../docs/SYNOLOGY_PROXY_SETUP.md) - 初回構築手順(旧版・参考用)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ アーキテクチャ
|
||||||
|
|
||||||
|
```
|
||||||
|
Flutter App (Client)
|
||||||
|
↓ HTTP POST /analyze
|
||||||
|
↓ Authorization: Bearer <token>
|
||||||
|
Proxy Server (Node.js/Express)
|
||||||
|
↓ Rate Limit Check (Redis)
|
||||||
|
↓ Gemini API Call
|
||||||
|
Gemini 2.5 Flash API
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 セキュリティ
|
||||||
|
|
||||||
|
- `.env` ファイルは **Git管理しない**(`.gitignore`に追加済み)
|
||||||
|
- `PROXY_AUTH_TOKEN` は **長いランダム文字列** を使用
|
||||||
|
- 外部公開する場合は **リバースプロキシ + HTTPS** 推奨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 環境変数
|
||||||
|
|
||||||
|
| 変数名 | 説明 | デフォルト |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `GEMINI_API_KEY` | Gemini APIキー(必須) | なし |
|
||||||
|
| `PROXY_AUTH_TOKEN` | Bearer Token認証(推奨) | なし(認証無効) |
|
||||||
|
| `DAILY_LIMIT` | 1日あたりのリクエスト上限 | 50 |
|
||||||
|
| `REDIS_HOST` | Redisホスト名 | `redis` |
|
||||||
|
| `REDIS_PORT` | Redisポート | `6379` |
|
||||||
|
| `PORT` | Proxy Serverポート | `8080` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 トラブルシューティング
|
||||||
|
|
||||||
|
### Q: `Redis connection failed`
|
||||||
|
|
||||||
|
**解決策**: Redisコンテナが起動していることを確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
# ponshu-redis が "Up (healthy)" になっているか確認
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Flutter アプリから接続できない
|
||||||
|
|
||||||
|
**解決策**:
|
||||||
|
1. `.env` の `PROXY_AUTH_TOKEN` を確認
|
||||||
|
2. Flutter の `lib/secrets.local.dart` で同じトークンを設定
|
||||||
|
3. Synology NASのIPアドレス(`100.76.7.3`)が正しいか確認
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-02-21
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
# Synology Proxy Server - Redis永続化移行ガイド
|
||||||
|
|
||||||
|
## 📋 概要
|
||||||
|
|
||||||
|
このガイドでは、Synology NAS上のProxy Serverを**In-Memory方式からRedis永続化方式**に移行する手順を説明します。
|
||||||
|
|
||||||
|
### 移行の目的
|
||||||
|
- **永続化**: Dockerコンテナ再起動後もレート制限カウントが維持される
|
||||||
|
- **Dokploy対応**: 将来のDokploy導入時にそのまま移行可能な構成
|
||||||
|
- **複数アプリ対応**: ダッシュボードWebシステム等、他プロジェクトでもRedisを共有可能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 前提条件
|
||||||
|
|
||||||
|
- Synology NAS(Container Manager インストール済み)
|
||||||
|
- 既存のProxy Serverコンテナが稼働中(`ponshu-proxy-server`)
|
||||||
|
- SSH接続可能な環境(または File Station経由のファイル操作)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 ステップ1: 既存コンテナの停止と削除
|
||||||
|
|
||||||
|
### 1-1. Container Managerで既存コンテナを停止
|
||||||
|
|
||||||
|
1. Synology DSM → **Container Manager** を開く
|
||||||
|
2. **コンテナ** タブで `ponshu-proxy-server` を選択
|
||||||
|
3. **アクション** → **停止** をクリック
|
||||||
|
4. 停止後、**削除** をクリック(イメージは削除しない)
|
||||||
|
|
||||||
|
**注意**: 既存のレート制限カウントは削除されますが、これは移行の性質上避けられません。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 ステップ2: 新しいファイルをSynologyに配置
|
||||||
|
|
||||||
|
### 2-1. File Stationでファイルをアップロード
|
||||||
|
|
||||||
|
PC上の `tools/proxy/` フォルダから、以下のファイルをSynologyの `/docker/ponshu_proxy/` にアップロードします。
|
||||||
|
|
||||||
|
**アップロード対象**:
|
||||||
|
- `docker-compose.yml` ← **新規追加**
|
||||||
|
- `server.js` ← **更新版**
|
||||||
|
- `package.json` ← **更新版(Redis依存追加)**
|
||||||
|
- `Dockerfile` ← 変更なし
|
||||||
|
- `.env` ← **新規作成(次ステップ)**
|
||||||
|
|
||||||
|
### 2-2. `.env` ファイルを作成
|
||||||
|
|
||||||
|
File Stationで `/docker/ponshu_proxy/.env` を新規作成し、以下の内容を記述します。
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Gemini API Key
|
||||||
|
GEMINI_API_KEY=AIzaSy...(あなたのAPIキー)
|
||||||
|
|
||||||
|
# Proxy認証トークン(必須推奨)
|
||||||
|
PROXY_AUTH_TOKEN=your-secure-token-here
|
||||||
|
|
||||||
|
# 1日あたりの解析回数上限
|
||||||
|
DAILY_LIMIT=50
|
||||||
|
|
||||||
|
# Redis接続設定(Docker Compose使用時はデフォルトでOK)
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
```
|
||||||
|
|
||||||
|
**セキュリティ注意**:
|
||||||
|
- `.env` ファイルは**Git管理しない**こと(`.gitignore` に追加済み)
|
||||||
|
- `PROXY_AUTH_TOKEN` は推測困難な長いランダム文字列を使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 ステップ3: Docker Composeでコンテナ起動
|
||||||
|
|
||||||
|
### 3-1. SSH接続でSynologyにログイン
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@100.76.7.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-2. プロジェクトディレクトリに移動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /volume1/docker/ponshu_proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-3. Docker Composeで起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**実行内容**:
|
||||||
|
- `ponshu-redis`: Redisコンテナを起動(ポート6379、永続化有効)
|
||||||
|
- `ponshu-proxy-server`: Proxy Serverコンテナを起動(ポート8080、Redis接続)
|
||||||
|
|
||||||
|
### 3-4. 起動確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**期待される出力**:
|
||||||
|
```
|
||||||
|
NAME IMAGE STATUS
|
||||||
|
ponshu-redis redis:7-alpine Up (healthy)
|
||||||
|
ponshu-proxy-server ponshu_proxy-proxy Up (healthy)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ステップ4: 動作確認
|
||||||
|
|
||||||
|
### 4-1. ヘルスチェック
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://100.76.7.3:8080/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**期待される出力**: `OK`
|
||||||
|
|
||||||
|
### 4-2. Redis接続確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redisコンテナに接続
|
||||||
|
sudo docker exec -it ponshu-redis redis-cli
|
||||||
|
|
||||||
|
# Redis内でコマンド実行
|
||||||
|
127.0.0.1:6379> PING
|
||||||
|
PONG
|
||||||
|
|
||||||
|
127.0.0.1:6379> KEYS *
|
||||||
|
(empty array) # 初回起動時は空
|
||||||
|
|
||||||
|
127.0.0.1:6379> exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-3. レート制限テスト(Flutter アプリから実行)
|
||||||
|
|
||||||
|
Flutter アプリ(`lib/secrets.local.dart`)で以下を設定:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
static const String aiProxyAnalyzeUrl = 'http://100.76.7.3:8080/analyze';
|
||||||
|
static const bool useProxy = true;
|
||||||
|
static const String proxyAuthToken = 'your-secure-token-here'; // .envと同じ値
|
||||||
|
```
|
||||||
|
|
||||||
|
アプリから日本酒ラベル解析を実行し、成功することを確認。
|
||||||
|
|
||||||
|
### 4-4. Redis内のデータ確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker exec -it ponshu-redis redis-cli
|
||||||
|
|
||||||
|
# 使用状況を確認(device_idは実際のデバイスIDに置き換え)
|
||||||
|
127.0.0.1:6379> KEYS usage:*
|
||||||
|
1) "usage:device-123:2026-02-21"
|
||||||
|
|
||||||
|
127.0.0.1:6379> GET "usage:device-123:2026-02-21"
|
||||||
|
"1" # 1回解析済み
|
||||||
|
|
||||||
|
127.0.0.1:6379> TTL "usage:device-123:2026-02-21"
|
||||||
|
(integer) 43200 # 残り12時間(秒単位)
|
||||||
|
```
|
||||||
|
|
||||||
|
**重要**: TTL(有効期限)が設定されていることを確認してください。これにより、翌日0時以降に自動的にリセットされます。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 ステップ5: コンテナ再起動テスト(永続化確認)
|
||||||
|
|
||||||
|
### 5-1. コンテナ再起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5-2. 再起動後のデータ確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker exec -it ponshu-redis redis-cli
|
||||||
|
|
||||||
|
127.0.0.1:6379> GET "usage:device-123:2026-02-21"
|
||||||
|
"1" # 再起動前のカウントが維持されている
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功条件**: 再起動後もカウントが保持されていること。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 運用上の注意点
|
||||||
|
|
||||||
|
### ログの確認方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Proxy Serverのログ
|
||||||
|
sudo docker-compose logs -f proxy
|
||||||
|
|
||||||
|
# Redisのログ
|
||||||
|
sudo docker-compose logs -f redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### コンテナの停止・削除
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止(データは保持)
|
||||||
|
sudo docker-compose stop
|
||||||
|
|
||||||
|
# 完全削除(データも削除される)
|
||||||
|
sudo docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**警告**: `docker-compose down -v` を実行すると、Redisのデータボリュームも削除されます。
|
||||||
|
|
||||||
|
### Redis データのバックアップ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redisのデータをダンプ
|
||||||
|
sudo docker exec ponshu-redis redis-cli SAVE
|
||||||
|
|
||||||
|
# ダンプファイルをコピー
|
||||||
|
sudo docker cp ponshu-redis:/data/dump.rdb /volume1/backups/redis-backup-$(date +%Y%m%d).rdb
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Dokploy移行への準備
|
||||||
|
|
||||||
|
この構成は**Dokploy環境にそのまま移行可能**です。
|
||||||
|
|
||||||
|
### Dokploy移行時の変更点
|
||||||
|
|
||||||
|
1. **Gitea連携**: Gitea pushで自動デプロイ
|
||||||
|
2. **環境変数管理**: `.env` ファイルの代わりにDokployのWeb UIで設定
|
||||||
|
3. **ドメイン設定**: `ponshu-proxy.local` 等のカスタムドメイン自動割り当て
|
||||||
|
4. **スケーリング**: Docker Swarmで複数ノードに分散可能
|
||||||
|
|
||||||
|
**移行時期**: Week 2(日本酒アプリとダッシュボードを同時にDokployへ移行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 トラブルシューティング
|
||||||
|
|
||||||
|
### Q1: Proxy Serverが起動しない(`Redis connection failed`)
|
||||||
|
|
||||||
|
**原因**: Redisコンテナが起動していない、またはネットワーク設定が誤っている。
|
||||||
|
|
||||||
|
**解決策**:
|
||||||
|
```bash
|
||||||
|
# Redisの状態確認
|
||||||
|
sudo docker-compose ps
|
||||||
|
|
||||||
|
# Redisが "unhealthy" の場合、ログを確認
|
||||||
|
sudo docker-compose logs redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: `docker-compose` コマンドが見つからない
|
||||||
|
|
||||||
|
**原因**: Synology DSMのバージョンによっては `docker compose`(スペース)を使用。
|
||||||
|
|
||||||
|
**解決策**:
|
||||||
|
```bash
|
||||||
|
sudo docker compose up -d # ハイフンなし
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: Flutter アプリから接続できない(認証エラー)
|
||||||
|
|
||||||
|
**原因**: `PROXY_AUTH_TOKEN` が `.env` と `secrets.local.dart` で一致していない。
|
||||||
|
|
||||||
|
**解決策**:
|
||||||
|
1. `.env` の `PROXY_AUTH_TOKEN` を確認
|
||||||
|
2. Flutter の `lib/secrets.local.dart` の `proxyAuthToken` を同じ値に設定
|
||||||
|
3. Flutter アプリを再起動
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 完了チェックリスト
|
||||||
|
|
||||||
|
- [ ] 既存コンテナを停止・削除
|
||||||
|
- [ ] 新しいファイル(`docker-compose.yml`, 更新版 `server.js`, `package.json`)をアップロード
|
||||||
|
- [ ] `.env` ファイルを作成(APIキー、認証トークン設定)
|
||||||
|
- [ ] `docker-compose up -d` で起動成功
|
||||||
|
- [ ] `curl http://100.76.7.3:8080/health` で `OK` 返却
|
||||||
|
- [ ] Redis内でデータが保存されることを確認(`KEYS usage:*`)
|
||||||
|
- [ ] コンテナ再起動後もデータが維持されることを確認
|
||||||
|
- [ ] Flutter アプリから解析が成功することを確認
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**移行完了後は、この手順書を `docs/` フォルダに移動してアーカイブしてください。**
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Redis: Rate Limit永続化用
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ponshu-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --appendonly yes # AOF永続化有効化
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- ponshu-network
|
||||||
|
|
||||||
|
# Proxy Server: Gemini API プロキシ
|
||||||
|
proxy:
|
||||||
|
build: .
|
||||||
|
container_name: ponshu-proxy-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||||
|
- PROXY_AUTH_TOKEN=${PROXY_AUTH_TOKEN}
|
||||||
|
- DAILY_LIMIT=${DAILY_LIMIT:-50}
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "node -e \"require('http').get('http://localhost:8080/health', (r) => { if (r.statusCode !== 200) process.exit(1); }).on('error', () => process.exit(1))\"" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- ponshu-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ponshu-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"@google/generative-ai": "^0.21.0"
|
"@google/generative-ai": "^0.21.0",
|
||||||
|
"redis": "^4.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||||
|
const { createClient } = require('redis');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
@ -8,9 +9,36 @@ const PORT = process.env.PORT || 8080;
|
||||||
const API_KEY = process.env.GEMINI_API_KEY;
|
const API_KEY = process.env.GEMINI_API_KEY;
|
||||||
const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN;
|
const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN;
|
||||||
|
|
||||||
// Rate Limiting (Simple In-Memory)
|
// Rate Limiting Configuration
|
||||||
const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10);
|
const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10);
|
||||||
const usageStore = {}; // { deviceId: { date: 'YYYY-MM-DD', count: 0 } }
|
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
|
||||||
|
const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10);
|
||||||
|
|
||||||
|
// Redis Client Setup
|
||||||
|
const redisClient = createClient({
|
||||||
|
socket: {
|
||||||
|
host: REDIS_HOST,
|
||||||
|
port: REDIS_PORT
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
console.error('[Redis] Connection Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('connect', () => {
|
||||||
|
console.log(`[Redis] Connected to ${REDIS_HOST}:${REDIS_PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize Redis connection
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await redisClient.connect();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Redis] Failed to connect:', err);
|
||||||
|
process.exit(1); // Exit if Redis is unavailable
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Authentication Middleware (skip for /health)
|
// Authentication Middleware (skip for /health)
|
||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
|
|
@ -35,44 +63,81 @@ function authMiddleware(req, res, next) {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global middleware: Auth first (skip /health), then body parser
|
// Global middleware: Body parser first, then auth (skip /health)
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path === '/health') return next();
|
if (req.path === '/health') return next();
|
||||||
authMiddleware(req, res, next);
|
authMiddleware(req, res, next);
|
||||||
});
|
});
|
||||||
app.use(bodyParser.json({ limit: '10mb' }));
|
|
||||||
|
|
||||||
// Gemini Client
|
// Gemini Client with JSON response configuration
|
||||||
const genAI = new GoogleGenerativeAI(API_KEY);
|
const genAI = new GoogleGenerativeAI(API_KEY);
|
||||||
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); // Flutter側(gemini_service.dart)と統一
|
const model = genAI.getGenerativeModel({
|
||||||
|
model: "gemini-2.5-flash", // Flutter側(gemini_service.dart)と統一
|
||||||
|
generationConfig: {
|
||||||
|
responseMimeType: "application/json", // Force JSON-only output
|
||||||
|
temperature: 0.2, // チャート一貫性向上のため(Flutter側と統一)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Helper: Get Today's Date String (YYYY-MM-DD)
|
// Helper: Get Today's Date String (YYYY-MM-DD)
|
||||||
function getTodayString() {
|
function getTodayString() {
|
||||||
return new Date().toISOString().split('T')[0];
|
return new Date().toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Check & Update Rate Limit
|
// Helper: Check & Update Rate Limit (Redis-based)
|
||||||
function checkRateLimit(deviceId) {
|
async function checkRateLimit(deviceId) {
|
||||||
const today = getTodayString();
|
const today = getTodayString();
|
||||||
|
const redisKey = `usage:${deviceId}:${today}`;
|
||||||
|
|
||||||
if (!usageStore[deviceId]) {
|
try {
|
||||||
usageStore[deviceId] = { date: today, count: 0 };
|
// Get current usage count
|
||||||
|
const currentCount = await redisClient.get(redisKey);
|
||||||
|
const count = currentCount ? parseInt(currentCount, 10) : 0;
|
||||||
|
const remaining = DAILY_LIMIT - count;
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: remaining > 0,
|
||||||
|
current: count,
|
||||||
|
limit: DAILY_LIMIT,
|
||||||
|
remaining: remaining,
|
||||||
|
redisKey: redisKey
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Redis] Error checking rate limit:', err);
|
||||||
|
// Fallback: deny request if Redis is down
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
current: 0,
|
||||||
|
limit: DAILY_LIMIT,
|
||||||
|
remaining: 0,
|
||||||
|
error: 'Rate limit check failed'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset if new day
|
// Helper: Increment Usage Count (Redis-based)
|
||||||
if (usageStore[deviceId].date !== today) {
|
async function incrementUsage(deviceId) {
|
||||||
usageStore[deviceId] = { date: today, count: 0 };
|
const today = getTodayString();
|
||||||
|
const redisKey = `usage:${deviceId}:${today}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Increment count
|
||||||
|
const newCount = await redisClient.incr(redisKey);
|
||||||
|
|
||||||
|
// Set expiration to end of day (86400 seconds = 24 hours)
|
||||||
|
const now = new Date();
|
||||||
|
const midnight = new Date(now);
|
||||||
|
midnight.setHours(24, 0, 0, 0);
|
||||||
|
const secondsUntilMidnight = Math.floor((midnight - now) / 1000);
|
||||||
|
|
||||||
|
await redisClient.expire(redisKey, secondsUntilMidnight);
|
||||||
|
|
||||||
|
return newCount;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Redis] Error incrementing usage:', err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUsage = usageStore[deviceId];
|
|
||||||
const remaining = DAILY_LIMIT - currentUsage.count;
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: remaining > 0,
|
|
||||||
current: currentUsage.count,
|
|
||||||
limit: DAILY_LIMIT,
|
|
||||||
remaining: remaining
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Endpoint (authentication enforced by global middleware)
|
// API Endpoint (authentication enforced by global middleware)
|
||||||
|
|
@ -83,20 +148,20 @@ app.post('/analyze', async (req, res) => {
|
||||||
return res.status(400).json({ success: false, error: 'Device ID is required' });
|
return res.status(400).json({ success: false, error: 'Device ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Check Rate Limit
|
|
||||||
const limitStatus = checkRateLimit(device_id);
|
|
||||||
if (!limitStatus.allowed) {
|
|
||||||
console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`);
|
|
||||||
return res.status(429).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Daily limit reached',
|
|
||||||
usage: { today: limitStatus.current, limit: limitStatus.limit }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. Check Rate Limit (Redis-based)
|
||||||
|
const limitStatus = await checkRateLimit(device_id);
|
||||||
|
if (!limitStatus.allowed) {
|
||||||
|
console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`);
|
||||||
|
return res.status(429).json({
|
||||||
|
success: false,
|
||||||
|
error: limitStatus.error || 'Daily limit reached',
|
||||||
|
usage: { today: limitStatus.current, limit: limitStatus.limit }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0} | Current: ${limitStatus.current}/${limitStatus.limit}`);
|
||||||
|
|
||||||
// 2. Prepare Gemini Request
|
// 2. Prepare Gemini Request
|
||||||
// Base64 images to GenerativeContentBlob
|
// Base64 images to GenerativeContentBlob
|
||||||
const imageParts = (images || []).map(base64 => ({
|
const imageParts = (images || []).map(base64 => ({
|
||||||
|
|
@ -111,32 +176,41 @@ app.post('/analyze', async (req, res) => {
|
||||||
const text = response.text();
|
const text = response.text();
|
||||||
|
|
||||||
// 3. Parse JSON from Markdown (e.g. ```json ... ```)
|
// 3. Parse JSON from Markdown (e.g. ```json ... ```)
|
||||||
|
console.log(`[Debug] Gemini raw response (first 200 chars): ${text.substring(0, 200)}`);
|
||||||
|
|
||||||
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/);
|
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/);
|
||||||
let jsonData;
|
let jsonData;
|
||||||
|
|
||||||
if (jsonMatch) {
|
if (jsonMatch) {
|
||||||
|
console.log('[Debug] Found JSON in code block');
|
||||||
jsonData = JSON.parse(jsonMatch[1]);
|
jsonData = JSON.parse(jsonMatch[1]);
|
||||||
} else {
|
} else {
|
||||||
// Try parsing raw text if no code blocks
|
// Try parsing raw text if no code blocks
|
||||||
jsonData = JSON.parse(text);
|
console.log('[Debug] Attempting to parse raw text as JSON');
|
||||||
|
try {
|
||||||
|
jsonData = JSON.parse(text);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('[Error] JSON parse failed. Raw response:', text);
|
||||||
|
throw new Error(`Failed to parse Gemini response as JSON. Response starts with: ${text.substring(0, 100)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update Usage
|
// 4. Increment Usage (Redis-based)
|
||||||
usageStore[device_id].count++;
|
const newCount = await incrementUsage(device_id);
|
||||||
console.log(`[Success] Device: ${device_id} | Count: ${usageStore[device_id].count}/${DAILY_LIMIT}`);
|
console.log(`[Success] Device: ${device_id} | New Count: ${newCount}/${DAILY_LIMIT}`);
|
||||||
|
|
||||||
// 5. Send Response
|
// 5. Send Response
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: jsonData,
|
data: jsonData,
|
||||||
usage: {
|
usage: {
|
||||||
today: usageStore[device_id].count,
|
today: newCount,
|
||||||
limit: DAILY_LIMIT
|
limit: DAILY_LIMIT
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gemini API Error:', error);
|
console.error('[Error] Gemini API or Redis Error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Internal Server Error'
|
error: error.message || 'Internal Server Error'
|
||||||
|
|
@ -150,7 +224,7 @@ app.get('/health', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start Server
|
// Start Server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Proxy Server running on port ${PORT}`);
|
console.log(`Proxy Server running on port ${PORT}`);
|
||||||
if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!');
|
if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!');
|
||||||
if (!AUTH_TOKEN) console.error('WARNING: PROXY_AUTH_TOKEN is not set! Authentication is disabled.');
|
if (!AUTH_TOKEN) console.error('WARNING: PROXY_AUTH_TOKEN is not set! Authentication is disabled.');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue