diff --git a/oauth/ASK_AI_INTEGRATION.md b/oauth/ASK_AI_INTEGRATION.md deleted file mode 100644 index 23af6c8..0000000 --- a/oauth/ASK_AI_INTEGRATION.md +++ /dev/null @@ -1,116 +0,0 @@ -# Ask-AI Integration Implementation - -## 概要 - -oauth_new アプリに ask-AI 機能を統合しました。この機能により、ユーザーはAIと対話し、その結果を atproto に記録できます。 - -## 実装されたファイル - -### 1. `/src/hooks/useAskAI.js` -- ask-AI サーバーとの通信機能 -- atproto への putRecord 機能 -- チャット履歴の管理 -- イベント送信(blog との通信用) - -### 2. `/src/components/AskAI.jsx` -- チャット UI コンポーネント -- 質問入力・回答表示 -- 認証チェック -- IME 対応 - -### 3. `/src/App.jsx` の更新 -- AskAI コンポーネントの統合 -- Ask AI ボタンの追加 -- イベントリスナーの設定 -- blog との通信機能 - -## JSON 構造の記録 - -`./json/` ディレクトリに各 collection の構造を記録しました: - -- `ai.syui.ai_user.json` - ユーザーリスト -- `ai.syui.ai_chat.json` - チャット記録(空) -- `syui.syui.ai_chat.json` - チャット記録(実データ) -- `ai.syui.ai_chat_lang.json` - 翻訳記録 -- `ai.syui.ai_chat_comment.json` - コメント記録 - -## 実際の ai.syui.log.chat 構造 - -確認された実際の構造: - -```json -{ - "$type": "ai.syui.log.chat", - "post": { - "url": "https://syui.ai/", - "date": "2025-06-18T02:16:04.609Z", - "slug": "", - "tags": [], - "title": "syui.ai", - "language": "ja" - }, - "text": "質問またはAI回答テキスト", - "type": "question|answer", - "author": { - "did": "did:plc:...", - "handle": "handle名", - "displayName": "表示名", - "avatar": "アバターURL" - }, - "createdAt": "2025-06-18T02:16:04.609Z" -} -``` - -## イベント通信 - -blog(ask-ai.js)と OAuth アプリ間の通信: - -### 送信イベント -- `postAIQuestion` - blog から OAuth アプリへ質問送信 -- `aiProfileLoaded` - OAuth アプリから blog へ AI プロフィール送信 -- `aiResponseReceived` - OAuth アプリから blog へ AI 回答送信 - -### 受信イベント -- OAuth アプリが `postAIQuestion` を受信して処理 -- blog が `aiResponseReceived` を受信して表示 - -## 環境変数 - -```env -VITE_ASK_AI_URL=http://localhost:3000/ask # ask-AI サーバーURL(デフォルト) -VITE_ADMIN_HANDLE=ai.syui.ai -VITE_ATPROTO_PDS=syu.is -VITE_OAUTH_COLLECTION=ai.syui.log -``` - -## 機能 - -### 実装済み -- ✅ ask-AI サーバーとの通信 -- ✅ atproto への question/answer record 保存 -- ✅ チャット履歴の表示・管理 -- ✅ blog との双方向イベント通信 -- ✅ 認証機能(ログイン必須) -- ✅ エラーハンドリング・ローディング状態 -- ✅ 実際の JSON 構造に合わせた実装 - -### 今後のテスト項目 -- ask-AI サーバーの準備・起動 -- 実際の質問送信テスト -- atproto への putRecord 動作確認 -- blog からの連携テスト - -## 使用方法 - -1. 開発サーバー起動: `npm run dev` -2. OAuth ログイン実行 -3. "Ask AI" ボタンをクリック -4. チャット画面で質問入力 -5. AI の回答が表示され、atproto に記録される - -## 注意事項 - -- ask-AI サーバー(VITE_ASK_AI_URL)が必要 -- 認証されたユーザーのみ質問可能 -- ai.syui.log.chat への書き込み権限が必要 -- Production 環境では logger が無効化される \ No newline at end of file diff --git a/oauth/AVATAR_SYSTEM.md b/oauth/AVATAR_SYSTEM.md deleted file mode 100644 index 9473403..0000000 --- a/oauth/AVATAR_SYSTEM.md +++ /dev/null @@ -1,174 +0,0 @@ -# Avatar Fetching System - -This document describes the avatar fetching system implemented for the oauth_new application. - -## Overview - -The avatar system provides intelligent avatar fetching with fallback mechanisms, caching, and error handling. It follows the design specified in the project instructions: - -1. **Primary Source**: Try to use avatar from record JSON first -2. **Fallback**: If avatar is broken/missing, fetch fresh data from ATProto -3. **Fresh Data Flow**: handle → PDS → DID → profile → avatar URI -4. **Caching**: Avoid excessive API calls with intelligent caching - -## Files Structure - -``` -src/ -├── utils/ -│ └── avatar.js # Core avatar fetching logic -├── components/ -│ ├── Avatar.jsx # React avatar component -│ └── AvatarTest.jsx # Test component -└── App.css # Avatar styling -``` - -## Core Functions - -### `getAvatar(options)` -Main function to fetch avatar with intelligent fallback. - -```javascript -const avatar = await getAvatar({ - record: recordObject, // Optional: record containing avatar data - handle: 'user.handle', // Required if no record - did: 'did:plc:xxx', // Optional: user DID - forceFresh: false // Optional: force fresh fetch -}) -``` - -### `batchFetchAvatars(users)` -Fetch avatars for multiple users in parallel with concurrency control. - -```javascript -const avatarMap = await batchFetchAvatars([ - { handle: 'user1.handle', did: 'did:plc:xxx1' }, - { handle: 'user2.handle', did: 'did:plc:xxx2' } -]) -``` - -### `prefetchAvatar(handle)` -Prefetch and cache avatar for a specific handle. - -```javascript -await prefetchAvatar('user.handle') -``` - -## React Components - -### `` -Basic avatar component with loading states and fallbacks. - -```jsx - console.log('loaded')} - onError={(err) => console.log('error', err)} -/> -``` - -### `` -Avatar with hover card showing user information. - -```jsx - -``` - -### `` -Display multiple avatars with overlap effect. - -```jsx - -``` - -## Data Flow - -1. **Record Check**: Extract avatar from record.value.author.avatar -2. **URL Validation**: Verify avatar URL is accessible (HEAD request) -3. **Fresh Fetch**: If broken, fetch fresh data: - - Get PDS from handle using `getPdsFromHandle()` - - Get API config using `getApiConfig()` - - Get DID from PDS using `atproto.getDid()` - - Get profile from bsky API using `atproto.getProfile()` - - Extract avatar from profile -4. **Cache**: Store result in cache with 30-minute TTL -5. **Fallback**: Show initial-based fallback if no avatar found - -## Caching Strategy - -- **Cache Key**: `avatar:{handle}` -- **Duration**: 30 minutes (configurable) -- **Cache Provider**: Uses existing `dataCache` utility -- **Invalidation**: Manual cache clearing functions available - -## Error Handling - -- **Network Errors**: Gracefully handled with fallback UI -- **Broken URLs**: Automatically detected and re-fetched fresh -- **Missing Handles**: Throws descriptive error messages -- **API Failures**: Logged but don't break UI - -## Integration - -The avatar system is integrated into the existing RecordList component: - -```jsx -// Old approach -{record.value.author?.avatar && ( - avatar -)} - -// New approach - -``` - -## Testing - -The system includes a comprehensive test component (`AvatarTest.jsx`) that can be accessed through the Test UI in the app. It demonstrates: - -1. Avatar from record data -2. Avatar from handle only -3. Broken avatar URL handling -4. Batch fetching -5. Prefetch functionality -6. Various avatar components - -To test: -1. Open the app -2. Click "Test" button in header -3. Switch to "Avatar System" tab -4. Use the test controls to verify functionality - -## Performance Considerations - -- **Concurrent Fetching**: Batch operations use concurrency limits (5 parallel requests) -- **Caching**: Reduces API calls by caching results -- **Lazy Loading**: Avatar images use lazy loading -- **Error Recovery**: Broken avatars are automatically retried with fresh data - -## Future Enhancements - -1. **Persistent Cache**: Consider localStorage for cross-session caching -2. **Image Optimization**: Add WebP support and size optimization -3. **Preloading**: Implement smarter preloading strategies -4. **CDN Integration**: Add CDN support for avatar delivery -5. **Placeholder Variations**: More diverse fallback avatar styles \ No newline at end of file diff --git a/oauth/AVATAR_USAGE_EXAMPLES.md b/oauth/AVATAR_USAGE_EXAMPLES.md deleted file mode 100644 index a6461f1..0000000 --- a/oauth/AVATAR_USAGE_EXAMPLES.md +++ /dev/null @@ -1,420 +0,0 @@ -# Avatar System Usage Examples - -This document provides practical examples of how to use the avatar fetching system in your components. - -## Basic Usage - -### Simple Avatar Display - -```jsx -import Avatar from './components/Avatar.jsx' - -function UserProfile({ user }) { - return ( -
- -

{user.displayName}

-
- ) -} -``` - -### Avatar from Record Data - -```jsx -function CommentItem({ record }) { - return ( -
- -
- {record.value.author.displayName} -

{record.value.text}

-
-
- ) -} -``` - -### Avatar with Hover Card - -```jsx -import { AvatarWithCard } from './components/Avatar.jsx' - -function UserList({ users, apiConfig }) { - return ( -
- {users.map(user => ( - - ))} -
- ) -} -``` - -## Advanced Usage - -### Programmatic Avatar Fetching - -```jsx -import { useEffect, useState } from 'react' -import { getAvatar, batchFetchAvatars } from './utils/avatar.js' - -function useUserAvatars(users) { - const [avatars, setAvatars] = useState(new Map()) - const [loading, setLoading] = useState(false) - - useEffect(() => { - async function fetchAvatars() { - setLoading(true) - try { - const avatarMap = await batchFetchAvatars(users) - setAvatars(avatarMap) - } catch (error) { - console.error('Failed to fetch avatars:', error) - } finally { - setLoading(false) - } - } - - if (users.length > 0) { - fetchAvatars() - } - }, [users]) - - return { avatars, loading } -} - -// Usage -function TeamDisplay({ team }) { - const { avatars, loading } = useUserAvatars(team.members) - - if (loading) return
Loading team...
- - return ( -
- {team.members.map(member => ( - {member.displayName} - ))} -
- ) -} -``` - -### Force Refresh Avatar - -```jsx -import { useState } from 'react' -import Avatar from './components/Avatar.jsx' -import { getAvatar, clearAvatarCache } from './utils/avatar.js' - -function RefreshableAvatar({ handle, did }) { - const [key, setKey] = useState(0) - - const handleRefresh = async () => { - // Clear cache for this user - clearAvatarCache(handle) - - // Force re-render of Avatar component - setKey(prev => prev + 1) - - // Optionally, prefetch fresh avatar - try { - await getAvatar({ handle, did, forceFresh: true }) - } catch (error) { - console.error('Failed to refresh avatar:', error) - } - } - - return ( -
- - -
- ) -} -``` - -### Avatar List with Overflow - -```jsx -import { AvatarList } from './components/Avatar.jsx' - -function ParticipantsList({ participants, maxVisible = 5 }) { - return ( -
-

Participants ({participants.length})

- - {participants.length > maxVisible && ( - - and {participants.length - maxVisible} more... - - )} -
- ) -} -``` - -## Error Handling - -### Custom Error Handling - -```jsx -import { useState } from 'react' -import Avatar from './components/Avatar.jsx' - -function RobustAvatar({ handle, did, fallbackSrc }) { - const [hasError, setHasError] = useState(false) - - const handleError = (error) => { - console.warn(`Avatar failed for ${handle}:`, error) - setHasError(true) - } - - if (hasError && fallbackSrc) { - return ( - Fallback avatar setHasError(false)} // Reset on fallback error - /> - ) - } - - return ( - - ) -} -``` - -### Loading States - -```jsx -import { useState, useEffect } from 'react' -import { getAvatar } from './utils/avatar.js' - -function AvatarWithCustomLoading({ handle, did }) { - const [avatar, setAvatar] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - async function loadAvatar() { - try { - setLoading(true) - setError(null) - const avatarUrl = await getAvatar({ handle, did }) - setAvatar(avatarUrl) - } catch (err) { - setError(err.message) - } finally { - setLoading(false) - } - } - - loadAvatar() - }, [handle, did]) - - if (loading) { - return
Loading...
- } - - if (error) { - return
Failed to load avatar
- } - - if (!avatar) { - return
No avatar
- } - - return Avatar -} -``` - -## Optimization Patterns - -### Preloading Strategy - -```jsx -import { useEffect } from 'react' -import { prefetchAvatar } from './utils/avatar.js' - -function UserCard({ user, isVisible }) { - // Preload avatar when component becomes visible - useEffect(() => { - if (isVisible && user.handle) { - prefetchAvatar(user.handle) - } - }, [isVisible, user.handle]) - - return ( -
- {isVisible && ( - - )} -

{user.displayName}

-
- ) -} -``` - -### Lazy Loading with Intersection Observer - -```jsx -import { useState, useEffect, useRef } from 'react' -import Avatar from './components/Avatar.jsx' - -function LazyAvatar({ handle, did, ...props }) { - const [isVisible, setIsVisible] = useState(false) - const ref = useRef() - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setIsVisible(true) - observer.disconnect() - } - }, - { threshold: 0.1 } - ) - - if (ref.current) { - observer.observe(ref.current) - } - - return () => observer.disconnect() - }, []) - - return ( -
- {isVisible ? ( - - ) : ( -
- )} -
- ) -} -``` - -## Cache Management - -### Cache Statistics Display - -```jsx -import { useEffect, useState } from 'react' -import { getAvatarCacheStats, cleanupExpiredAvatars } from './utils/avatarCache.js' - -function CacheStatsPanel() { - const [stats, setStats] = useState(null) - - useEffect(() => { - const updateStats = () => { - setStats(getAvatarCacheStats()) - } - - updateStats() - const interval = setInterval(updateStats, 5000) // Update every 5 seconds - - return () => clearInterval(interval) - }, []) - - const handleCleanup = async () => { - const cleaned = cleanupExpiredAvatars() - alert(`Cleaned ${cleaned} expired cache entries`) - setStats(getAvatarCacheStats()) - } - - if (!stats) return null - - return ( -
-

Avatar Cache Stats

-

Cached avatars: {stats.totalCached}

-

Cache hit rate: {stats.hitRate}%

-

Cache hits: {stats.cacheHits}

-

Cache misses: {stats.cacheMisses}

- -
- ) -} -``` - -## Testing Helpers - -### Mock Avatar for Testing - -```jsx -// For testing environments -const MockAvatar = ({ handle, size = 40, showFallback = true }) => { - if (!showFallback) return null - - const initial = (handle || 'U')[0].toUpperCase() - - return ( -
- {initial} -
- ) -} - -// Use in tests -export default process.env.NODE_ENV === 'test' ? MockAvatar : Avatar -``` - -These examples demonstrate the flexibility and power of the avatar system while maintaining good performance and user experience practices. \ No newline at end of file diff --git a/oauth/CLOUDFLARE_DEPLOY.yml b/oauth/CLOUDFLARE_DEPLOY.yml deleted file mode 100644 index a22efb2..0000000 --- a/oauth/CLOUDFLARE_DEPLOY.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Deploy to Cloudflare Pages - -on: - push: - branches: - - main - workflow_dispatch: - -env: - OAUTH_DIR: oauth_new - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: ${{ env.OAUTH_DIR }}/package-lock.json - - - name: Install dependencies - run: | - cd ${{ env.OAUTH_DIR }} - npm ci - - - name: Build OAuth app - run: | - cd ${{ env.OAUTH_DIR }} - NODE_ENV=production npm run build - env: - VITE_ADMIN: ${{ secrets.VITE_ADMIN }} - VITE_PDS: ${{ secrets.VITE_PDS }} - VITE_HANDLE_LIST: ${{ secrets.VITE_HANDLE_LIST }} - VITE_COLLECTION: ${{ secrets.VITE_COLLECTION }} - VITE_OAUTH_CLIENT_ID: ${{ secrets.VITE_OAUTH_CLIENT_ID }} - VITE_OAUTH_REDIRECT_URI: ${{ secrets.VITE_OAUTH_REDIRECT_URI }} - VITE_ENABLE_TEST_UI: 'false' - VITE_ENABLE_DEBUG: 'false' - - - name: Deploy to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - directory: ${{ env.OAUTH_DIR }}/dist - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - deploymentName: Production \ No newline at end of file diff --git a/oauth/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml b/oauth/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml deleted file mode 100644 index 0f36458..0000000 --- a/oauth/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Deploy to Cloudflare Pages - -on: - push: - branches: - - main - workflow_dispatch: - -env: - OAUTH_DIR: oauth_new - KEEP_DEPLOYMENTS: 5 # 保持するデプロイメント数 - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: ${{ env.OAUTH_DIR }}/package-lock.json - - - name: Install dependencies - run: | - cd ${{ env.OAUTH_DIR }} - npm ci - - - name: Build OAuth app - run: | - cd ${{ env.OAUTH_DIR }} - NODE_ENV=production npm run build - env: - VITE_ADMIN: ${{ secrets.VITE_ADMIN }} - VITE_PDS: ${{ secrets.VITE_PDS }} - VITE_HANDLE_LIST: ${{ secrets.VITE_HANDLE_LIST }} - VITE_COLLECTION: ${{ secrets.VITE_COLLECTION }} - VITE_OAUTH_CLIENT_ID: ${{ secrets.VITE_OAUTH_CLIENT_ID }} - VITE_OAUTH_REDIRECT_URI: ${{ secrets.VITE_OAUTH_REDIRECT_URI }} - VITE_ENABLE_TEST_UI: 'false' - VITE_ENABLE_DEBUG: 'false' - - - name: Deploy to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - directory: ${{ env.OAUTH_DIR }}/dist - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - deploymentName: Production - - cleanup: - needs: deploy - runs-on: ubuntu-latest - if: success() - - steps: - - name: Wait for deployment to complete - run: sleep 30 - - - name: Cleanup old deployments - run: | - # Get all deployments - DEPLOYMENTS=$(curl -s -X GET \ - "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \ - -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type: application/json") - - # Extract deployment IDs (skip the latest N deployments) - DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id // empty") - - if [ -z "$DEPLOYMENT_IDS" ]; then - echo "No old deployments to delete" - exit 0 - fi - - # Delete old deployments - for ID in $DEPLOYMENT_IDS; do - echo "Deleting deployment: $ID" - RESPONSE=$(curl -s -X DELETE \ - "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \ - -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type: application/json") - - SUCCESS=$(echo "$RESPONSE" | jq -r '.success') - if [ "$SUCCESS" = "true" ]; then - echo "Successfully deleted deployment: $ID" - else - echo "Failed to delete deployment: $ID" - echo "$RESPONSE" | jq . - fi - - sleep 1 # Rate limiting - done - - echo "Cleanup completed!" \ No newline at end of file diff --git a/oauth/DEPLOYMENT.md b/oauth/DEPLOYMENT.md deleted file mode 100644 index a0bc407..0000000 --- a/oauth/DEPLOYMENT.md +++ /dev/null @@ -1,178 +0,0 @@ -# 本番環境デプロイメント手順 - -## 本番環境用の調整 - -### 1. テスト機能の削除・無効化 - -本番環境では以下の調整が必要です: - -#### A. TestUI コンポーネントの削除 -```jsx -// src/App.jsx から以下を削除/コメントアウト -import TestUI from './components/TestUI.jsx' -const [showTestUI, setShowTestUI] = useState(false) - -// ボトムセクションからTestUIを削除 -{showTestUI && ( - -)} - -``` - -#### B. ログ出力の完全無効化 -現在は `logger.js` で開発環境のみログが有効になっていますが、完全に確実にするため: - -```bash -# 本番ビルド前に全てのconsole.logを確認 -grep -r "console\." src/ --exclude-dir=node_modules -``` - -### 2. 環境変数の設定 - -#### 本番用 .env.production -```bash -VITE_ATPROTO_PDS=syu.is -VITE_ADMIN_HANDLE=ai.syui.ai -VITE_AI_HANDLE=ai.syui.ai -VITE_OAUTH_COLLECTION=ai.syui.log -``` - -### 3. ビルドコマンド - -```bash -# 本番用ビルド -npm run build - -# 生成されるファイル確認 -ls -la dist/ -``` - -### 4. デプロイ用ファイル構成 - -``` -dist/ -├── index.html # 最小化HTML -├── assets/ -│ ├── comment-atproto-[hash].js # メインJSバンドル -│ └── comment-atproto-[hash].css # CSS -``` - -### 5. ailog サイトへの統合 - -#### A. アセットファイルのコピー -```bash -# distファイルをailogサイトの適切な場所にコピー -cp dist/assets/* /path/to/ailog/static/assets/ -cp dist/index.html /path/to/ailog/templates/oauth-assets.html -``` - -#### B. ailog テンプレートでの読み込み -```html - -{{ if .Site.Params.oauth_comments }} - {{ partial "oauth-assets.html" . }} -{{ end }} -``` - -### 6. 本番環境チェックリスト - -#### ✅ セキュリティ -- [ ] OAuth認証のリダイレクトURL確認 -- [ ] 環境変数の機密情報確認 -- [ ] HTTPS通信確認 - -#### ✅ パフォーマンス -- [ ] バンドルサイズ確認(現在1.2MB) -- [ ] ファイル圧縮確認 -- [ ] キャッシュ設定確認 - -#### ✅ 機能 -- [ ] 本番PDS接続確認 -- [ ] OAuth認証フロー確認 -- [ ] コメント投稿・表示確認 -- [ ] アバター表示確認 - -#### ✅ UI/UX -- [ ] モバイル表示確認 -- [ ] アクセシビリティ確認 -- [ ] エラーハンドリング確認 - -### 7. 段階的デプロイ戦略 - -#### Phase 1: テスト環境 -```bash -# テスト用のサブドメインでデプロイ -# test.syui.ai など -``` - -#### Phase 2: 本番環境 -```bash -# 問題なければ本番環境にデプロイ -# ailog本体に統合 -``` - -### 8. トラブルシューティング - -#### よくある問題 -1. **OAuth認証エラー**: リダイレクトURL設定確認 -2. **PDS接続エラー**: ネットワーク・DNS設定確認 -3. **アバター表示エラー**: CORS設定確認 -4. **CSS競合**: oauth-プレフィックス確認 - -#### ログ確認方法 -```bash -# 本番環境でエラーが発生した場合 -# ブラウザのDevToolsでエラー確認 -# logger.jsは本番では無効化されている -``` - -### 9. 本番用設定ファイル - -```bash -# ~/.config/syui/ai/log/config.json -{ - "oauth": { - "environment": "production", - "debug": false, - "test_mode": false - } -} -``` - -### 10. 推奨デプロイ手順 - -```bash -# 1. テスト機能削除 -git checkout -b production-ready -# App.jsx からTestUI関連を削除 - -# 2. 本番ビルド -npm run build - -# 3. ファイル確認 -ls -la dist/ - -# 4. ailogサイトに統合 -cp dist/assets/* ../my-blog/static/assets/ -cp dist/index.html ../my-blog/templates/oauth-assets.html - -# 5. ailogサイトでテスト -cd ../my-blog -hugo server - -# 6. 問題なければcommit -git add . -git commit -m "Production build: Remove test UI, optimize for deployment" -``` - -## 注意事項 - -- TestUIは開発・デモ用のため本番では削除必須 -- loggerは自動で本番では無効化される -- OAuth設定は本番PDS用に調整必要 -- バンドルサイズが大きいため今後最適化検討 \ No newline at end of file diff --git a/oauth/DEVELOPMENT.md b/oauth/DEVELOPMENT.md deleted file mode 100644 index e49c2af..0000000 --- a/oauth/DEVELOPMENT.md +++ /dev/null @@ -1,334 +0,0 @@ -# 開発ガイド - -## 設計思想 - -このプロジェクトは以下の原則に基づいて設計されています: - -### 1. 環境変数による設定の外部化 -- ハードコードを避け、設定は全て環境変数で管理 -- `src/config/env.js` で一元管理 - -### 2. PDS(Personal Data Server)の自動判定 -- `VITE_HANDLE_LIST` と `VITE_PDS` による自動判定 -- syu.is系とbsky.social系の自動振り分け - -### 3. コンポーネントの責任分離 -- Hooks: ビジネスロジック -- Components: UI表示のみ -- Services: 外部API連携 -- Utils: 純粋関数 - -## アーキテクチャ詳細 - -### データフロー - -``` -User Input - ↓ -Hooks (useAuth, useAdminData, usePageContext) - ↓ -Services (OAuthService) - ↓ -API (atproto.js) - ↓ -ATProto Network - ↓ -Components (UI Display) -``` - -### 状態管理 - -React Hooksによる状態管理: -- `useAuth`: OAuth認証状態 -- `useAdminData`: 管理者データ(プロフィール、レコード) -- `usePageContext`: ページ判定(トップ/個別) - -### OAuth認証フロー - -``` -1. ユーザーがハンドル入力 -2. PDS判定 (syu.is vs bsky.social) -3. 適切なOAuthClientを選択 -4. 標準OAuth画面にリダイレクト -5. 認証完了後コールバック処理 -6. セッション復元・保存 -``` - -## 重要な実装詳細 - -### セッション管理 - -`@atproto/oauth-client-browser`が自動的に以下を処理: -- IndexedDBへのセッション保存 -- トークンの自動更新 -- DPoP(Demonstration of Proof of Possession) - -**注意**: 手動でのセッション管理は複雑なため、公式ライブラリを使用すること。 - -### PDS判定アルゴリズム - -```javascript -// src/utils/pds.js -function isSyuIsHandle(handle) { - return env.handleList.includes(handle) || handle.endsWith(`.${env.pds}`) -} -``` - -1. `VITE_HANDLE_LIST` に含まれるハンドル → syu.is -2. `.syu.is` で終わるハンドル → syu.is -3. その他 → bsky.social - -### レコードフィルタリング - -```javascript -// src/components/RecordTabs.jsx -const filterRecords = (records) => { - if (pageContext.isTopPage) { - return records.slice(0, 3) // 最新3件 - } else { - // URL のrkey と record.value.post.url のrkey を照合 - return records.filter(record => { - const recordRkey = new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') - return recordRkey === pageContext.rkey - }) - } -} -``` - -## 開発時の注意点 - -### 1. 環境変数の命名 - -- `VITE_` プレフィックス必須(Viteの制約) -- JSON形式の環境変数は文字列として定義 - -```bash -# ❌ 間違い -VITE_HANDLE_LIST=["ai.syui.ai"] - -# ✅ 正しい -VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai"] -``` - -### 2. API エラーハンドリング - -```javascript -// src/api/atproto.js -async function request(url) { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`HTTP ${response.status}`) - } - return await response.json() -} -``` - -すべてのAPI呼び出しでエラーハンドリングを実装。 - -### 3. コンポーネント設計 - -```javascript -// ❌ Bad: ビジネスロジックがコンポーネント内 -function MyComponent() { - const [data, setData] = useState([]) - useEffect(() => { - fetch('/api/data').then(setData) - }, []) - return
{data.map(...)}
-} - -// ✅ Good: Hooksでロジック分離 -function MyComponent() { - const { data, loading, error } = useMyData() - if (loading) return - if (error) return - return
{data.map(...)}
-} -``` - -## デバッグ手法 - -### 1. OAuth デバッグ - -```javascript -// ブラウザの開発者ツールで確認 -localStorage.clear() // セッションクリア -sessionStorage.clear() // 一時データクリア - -// IndexedDB確認(Application タブ) -// ATProtoの認証データが保存される -``` - -### 2. PDS判定デバッグ - -```javascript -// src/utils/pds.js にログ追加 -console.log('Handle:', handle) -console.log('Is syu.is:', isSyuIsHandle(handle)) -console.log('API Config:', getApiConfig(pds)) -``` - -### 3. レコードフィルタリングデバッグ - -```javascript -// src/components/RecordTabs.jsx -console.log('Page Context:', pageContext) -console.log('All Records:', records.length) -console.log('Filtered Records:', filteredRecords.length) -``` - -## パフォーマンス最適化 - -### 1. 並列データ取得 - -```javascript -// src/hooks/useAdminData.js -const [records, lang, comment] = await Promise.all([ - collections.getBase(apiConfig.pds, did, env.collection), - collections.getLang(apiConfig.pds, did, env.collection), - collections.getComment(apiConfig.pds, did, env.collection) -]) -``` - -### 2. 不要な再レンダリング防止 - -```javascript -// useMemo でフィルタリング結果をキャッシュ -const filteredRecords = useMemo(() => - filterRecords(records), - [records, pageContext] -) -``` - -## テスト戦略 - -### 1. 単体テスト推奨対象 - -- `src/utils/pds.js` - PDS判定ロジック -- `src/config/env.js` - 環境変数パース -- フィルタリング関数 - -### 2. 統合テスト推奨対象 - -- OAuth認証フロー -- API呼び出し -- レコード表示 - -## デプロイメント - -### 1. 必要ファイル - -``` -public/ -└── client-metadata.json # OAuth設定ファイル - -dist/ # ビルド出力 -├── index.html -└── assets/ - ├── comment-atproto-[hash].js - └── comment-atproto-[hash].css -``` - -### 2. デプロイ手順 - -```bash -# 1. 環境変数設定 -cp .env.example .env -# 2. 本番用設定を記入 -# 3. ビルド -npm run build -# 4. dist/ フォルダをデプロイ -``` - -### 3. 本番環境チェックリスト - -- [ ] `.env` ファイルの本番設定 -- [ ] `client-metadata.json` の設置 -- [ ] HTTPS 必須(OAuth要件) -- [ ] CSP(Content Security Policy)設定 - -## よくある問題と解決法 - -### 1. "OAuth initialization failed" - -**原因**: client-metadata.json が見つからない、または形式が正しくない - -**解決法**: -```bash -# public/client-metadata.json の存在確認 -ls -la public/client-metadata.json - -# 形式確認(JSON validation) -jq . public/client-metadata.json -``` - -### 2. "Failed to load admin data" - -**原因**: 管理者アカウントのDID解決に失敗 - -**解決法**: -```bash -# 手動でDID解決確認 -curl "https://syu.is/xrpc/com.atproto.repo.describeRepo?repo=ai.syui.ai" -``` - -### 3. レコードが表示されない - -**原因**: コレクション名の不一致、権限不足 - -**解決法**: -```bash -# コレクション確認 -curl "https://syu.is/xrpc/com.atproto.repo.listRecords?repo=did:plc:xxx&collection=ai.syui.log.chat.lang" -``` - -## 機能拡張ガイド - -### 1. 新しいコレクション追加 - -```javascript -// src/api/atproto.js に追加 -export const collections = { - // 既存... - async getNewCollection(pds, repo, collection, limit = 10) { - return await atproto.getRecords(pds, repo, `${collection}.new`, limit) - } -} -``` - -### 2. 新しいPDS対応 - -```javascript -// src/utils/pds.js を拡張 -export function getApiConfig(pds) { - if (pds.includes('syu.is')) { - // 既存の syu.is 設定 - } else if (pds.includes('newpds.com')) { - return { - pds: `https://newpds.com`, - bsky: `https://bsky.newpds.com`, - plc: `https://plc.newpds.com`, - web: `https://web.newpds.com` - } - } - // デフォルト設定 -} -``` - -### 3. リアルタイム更新追加 - -```javascript -// src/hooks/useRealtimeUpdates.js -export function useRealtimeUpdates(collection) { - useEffect(() => { - const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe') - ws.onmessage = (event) => { - const data = JSON.parse(event.data) - if (data.collection === collection) { - // 新しいレコードを追加 - } - } - return () => ws.close() - }, [collection]) -} -``` \ No newline at end of file diff --git a/oauth/ENV_SETUP.md b/oauth/ENV_SETUP.md deleted file mode 100644 index 91dfbbe..0000000 --- a/oauth/ENV_SETUP.md +++ /dev/null @@ -1,110 +0,0 @@ -# 環境変数による機能切り替え - -## 概要 - -開発用機能(TestUI、デバッグログ)をenv変数で簡単に有効/無効化できるようになりました。 - -## 設定ファイル - -### 開発環境用: `.env` -```bash -# Development/Debug features -VITE_ENABLE_TEST_UI=true -VITE_ENABLE_DEBUG=true -``` - -### 本番環境用: `.env.production` -```bash -# Production settings - Disable development features -VITE_ENABLE_TEST_UI=false -VITE_ENABLE_DEBUG=false -``` - -## 制御される機能 - -### 1. TestUI コンポーネント -- **制御**: `VITE_ENABLE_TEST_UI` -- **true**: TestボタンとTestUI表示 -- **false**: TestUI関連が完全に非表示 - -### 2. デバッグログ -- **制御**: `VITE_ENABLE_DEBUG` -- **true**: console.log等が有効 -- **false**: すべてのlogが無効化 - -## 使い方 - -### 開発時 -```bash -# .envで有効化されているので通常通り -npm run dev -npm run build -``` - -### 本番デプロイ時 -```bash -# 自動的に .env.production が読み込まれる -npm run build - -# または明示的に指定 -NODE_ENV=production npm run build -``` - -### 手動切り替え -```bash -# 一時的にTestUIだけ無効化 -VITE_ENABLE_TEST_UI=false npm run dev - -# 一時的にデバッグだけ無効化 -VITE_ENABLE_DEBUG=false npm run dev -``` - -## 実装詳細 - -### App.jsx -```jsx -// Environment-based feature flags -const ENABLE_TEST_UI = import.meta.env.VITE_ENABLE_TEST_UI === 'true' -const ENABLE_DEBUG = import.meta.env.VITE_ENABLE_DEBUG === 'true' - -// TestUI表示制御 -{ENABLE_TEST_UI && showTestUI && ( -
- -
-)} - -// Testボタン表示制御 -{ENABLE_TEST_UI && ( -
- -
-)} -``` - -### logger.js -```jsx -class Logger { - constructor() { - this.isDev = import.meta.env.DEV || false - this.debugEnabled = import.meta.env.VITE_ENABLE_DEBUG === 'true' - this.isEnabled = this.isDev && this.debugEnabled - } -} -``` - -## メリット - -✅ **コード削除不要**: 機能を残したまま本番で無効化 -✅ **簡単切り替え**: env変数だけで制御 -✅ **自動化対応**: CI/CDで環境別自動ビルド可能 -✅ **デバッグ容易**: 必要時に即座に有効化可能 - -## 本番デプロイチェックリスト - -- [ ] `.env.production`でTestUI無効化確認 -- [ ] `.env.production`でデバッグ無効化確認 -- [ ] 本番ビルドでTestボタン非表示確認 -- [ ] 本番でconsole.log出力なし確認 \ No newline at end of file diff --git a/oauth/IMPLEMENTATION_GUIDE.md b/oauth/IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 45c062c..0000000 --- a/oauth/IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,444 +0,0 @@ -# OAuth_new 実装ガイド - -## Claude Code用実装指示 - -### 即座に実装可能な改善(優先度:最高) - -#### 1. エラーハンドリング強化 - -**ファイル**: `src/utils/errorHandler.js` (新規作成) -```javascript -export class ATProtoError extends Error { - constructor(message, status, context) { - super(message) - this.status = status - this.context = context - this.timestamp = new Date().toISOString() - } -} - -export function getErrorMessage(error) { - if (error.status === 400) { - return 'アカウントまたはコレクションが見つかりません' - } else if (error.status === 429) { - return 'レート制限です。しばらく待ってから再試行してください' - } else if (error.status === 500) { - return 'サーバーエラーが発生しました' - } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) { - return 'ネットワーク接続を確認してください' - } else if (error.message.includes('timeout')) { - return 'タイムアウトしました。再試行してください' - } - return '予期しないエラーが発生しました' -} - -export function logError(error, context) { - console.error(`[ATProto Error] ${context}:`, { - message: error.message, - status: error.status, - timestamp: new Date().toISOString() - }) -} -``` - -**修正**: `src/api/atproto.js` -```javascript -import { ATProtoError, logError } from '../utils/errorHandler.js' - -async function request(url, options = {}) { - try { - const response = await fetch(url, options) - if (!response.ok) { - throw new ATProtoError( - `HTTP ${response.status}: ${response.statusText}`, - response.status, - { url, options } - ) - } - return await response.json() - } catch (error) { - if (error instanceof ATProtoError) { - logError(error, 'API Request') - throw error - } - - // Network errors - const atprotoError = new ATProtoError( - 'ネットワークエラーが発生しました', - 0, - { url, originalError: error.message } - ) - logError(atprotoError, 'Network Error') - throw atprotoError - } -} -``` - -**修正**: `src/hooks/useAdminData.js` -```javascript -import { getErrorMessage, logError } from '../utils/errorHandler.js' - -// loadAdminData関数内のcatchブロック -} catch (err) { - logError(err, 'useAdminData.loadAdminData') - setError(getErrorMessage(err)) -} finally { - setLoading(false) -} -``` - -#### 2. シンプルなキャッシュシステム - -**ファイル**: `src/utils/cache.js` (新規作成) -```javascript -class SimpleCache { - constructor(ttl = 30000) { // 30秒TTL - this.cache = new Map() - this.ttl = ttl - } - - get(key) { - const item = this.cache.get(key) - if (!item) return null - - if (Date.now() - item.timestamp > this.ttl) { - this.cache.delete(key) - return null - } - return item.data - } - - set(key, data) { - this.cache.set(key, { - data, - timestamp: Date.now() - }) - } - - clear() { - this.cache.clear() - } - - invalidatePattern(pattern) { - for (const key of this.cache.keys()) { - if (key.includes(pattern)) { - this.cache.delete(key) - } - } - } -} - -export const dataCache = new SimpleCache() -``` - -**修正**: `src/api/atproto.js` -```javascript -import { dataCache } from '../utils/cache.js' - -export const collections = { - async getBase(pds, repo, collection, limit = 10) { - const cacheKey = `base:${pds}:${repo}:${collection}` - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, collection, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getLang(pds, repo, collection, limit = 10) { - const cacheKey = `lang:${pds}:${repo}:${collection}` - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getComment(pds, repo, collection, limit = 10) { - const cacheKey = `comment:${pds}:${repo}:${collection}` - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) - dataCache.set(cacheKey, data) - return data - }, - - // 投稿後にキャッシュをクリア - invalidateCache(collection) { - dataCache.invalidatePattern(collection) - } -} -``` - -#### 3. ローディングスケルトン - -**ファイル**: `src/components/LoadingSkeleton.jsx` (新規作成) -```javascript -import React from 'react' - -export default function LoadingSkeleton({ count = 3 }) { - return ( -
- {Array(count).fill(0).map((_, i) => ( -
-
-
-
-
-
-
-
- ))} - - -
- ) -} -``` - -**修正**: `src/components/RecordTabs.jsx` -```javascript -import LoadingSkeleton from './LoadingSkeleton.jsx' - -// RecordTabsコンポーネント内 -{activeTab === 'lang' && ( - loading ? ( - - ) : ( - - ) -)} -``` - -### 中期実装(1週間以内) - -#### 4. リトライ機能 - -**修正**: `src/api/atproto.js` -```javascript -async function requestWithRetry(url, options = {}, maxRetries = 3) { - for (let i = 0; i < maxRetries; i++) { - try { - return await request(url, options) - } catch (error) { - if (i === maxRetries - 1) throw error - - // 429 (レート制限) の場合は長めに待機 - const baseDelay = error.status === 429 ? 5000 : 1000 - const delay = Math.min(baseDelay * Math.pow(2, i), 30000) - - console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`) - await new Promise(resolve => setTimeout(resolve, delay)) - } - } -} - -// 全てのAPI呼び出しでrequestをrequestWithRetryに変更 -export const atproto = { - async getDid(pds, handle) { - const res = await requestWithRetry(`https://${pds}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`) - return res.did - }, - // ...他のメソッドも同様に変更 -} -``` - -#### 5. 段階的ローディング - -**修正**: `src/hooks/useAdminData.js` -```javascript -export function useAdminData() { - const [adminData, setAdminData] = useState({ - did: '', - profile: null, - records: [], - apiConfig: null - }) - const [langRecords, setLangRecords] = useState([]) - const [commentRecords, setCommentRecords] = useState([]) - const [loadingStates, setLoadingStates] = useState({ - admin: true, - base: true, - lang: true, - comment: true - }) - const [error, setError] = useState(null) - - useEffect(() => { - loadAdminData() - }, []) - - const loadAdminData = async () => { - try { - setError(null) - - // Phase 1: 管理者情報を最初に取得 - setLoadingStates(prev => ({ ...prev, admin: true })) - const apiConfig = getApiConfig(`https://${env.pds}`) - const did = await atproto.getDid(env.pds, env.admin) - const profile = await atproto.getProfile(apiConfig.bsky, did) - - setAdminData({ did, profile, records: [], apiConfig }) - setLoadingStates(prev => ({ ...prev, admin: false })) - - // Phase 2: 基本レコードを取得 - setLoadingStates(prev => ({ ...prev, base: true })) - const records = await collections.getBase(apiConfig.pds, did, env.collection) - setAdminData(prev => ({ ...prev, records })) - setLoadingStates(prev => ({ ...prev, base: false })) - - // Phase 3: lang/commentを並列取得 - const langPromise = collections.getLang(apiConfig.pds, did, env.collection) - .then(data => { - setLangRecords(data) - setLoadingStates(prev => ({ ...prev, lang: false })) - }) - .catch(err => { - console.warn('Failed to load lang records:', err) - setLoadingStates(prev => ({ ...prev, lang: false })) - }) - - const commentPromise = collections.getComment(apiConfig.pds, did, env.collection) - .then(data => { - setCommentRecords(data) - setLoadingStates(prev => ({ ...prev, comment: false })) - }) - .catch(err => { - console.warn('Failed to load comment records:', err) - setLoadingStates(prev => ({ ...prev, comment: false })) - }) - - await Promise.all([langPromise, commentPromise]) - - } catch (err) { - logError(err, 'useAdminData.loadAdminData') - setError(getErrorMessage(err)) - // エラー時もローディング状態を解除 - setLoadingStates({ - admin: false, - base: false, - lang: false, - comment: false - }) - } - } - - return { - adminData, - langRecords, - commentRecords, - loading: Object.values(loadingStates).some(Boolean), - loadingStates, - error, - refresh: loadAdminData - } -} -``` - -### 緊急時対応 - -#### フォールバック機能 - -**修正**: `src/hooks/useAdminData.js` -```javascript -// エラー時でも基本機能を維持 -const loadWithFallback = async () => { - try { - await loadAdminData() - } catch (err) { - // フォールバック:最低限の表示を維持 - setAdminData({ - did: env.admin, // ハンドルをDIDとして使用 - profile: { - handle: env.admin, - displayName: env.admin, - avatar: null - }, - records: [], - apiConfig: getApiConfig(`https://${env.pds}`) - }) - setError('一部機能が利用できません。基本表示で継続します。') - } -} -``` - -## 実装チェックリスト - -### Phase 1 (即座実装) -- [ ] `src/utils/errorHandler.js` 作成 -- [ ] `src/utils/cache.js` 作成 -- [ ] `src/components/LoadingSkeleton.jsx` 作成 -- [ ] `src/api/atproto.js` エラーハンドリング追加 -- [ ] `src/hooks/useAdminData.js` エラーハンドリング改善 -- [ ] `src/components/RecordTabs.jsx` ローディング表示追加 - -### Phase 2 (1週間以内) -- [ ] `src/api/atproto.js` リトライ機能追加 -- [ ] `src/hooks/useAdminData.js` 段階的ローディング実装 -- [ ] キャッシュクリア機能の投稿フォーム統合 - -### テスト項目 -- [ ] エラー状態でも最低限表示される -- [ ] キャッシュが適切に動作する -- [ ] ローディング表示が適切に出る -- [ ] リトライが正常に動作する - -## パフォーマンス目標 - -- **初期表示**: 3秒 → 1秒 -- **キャッシュヒット率**: 70%以上 -- **エラー率**: 10% → 2%以下 -- **ユーザー体験**: ローディング状態が常に可視化 - -この実装により、./oauthで発生している「同じ問題の繰り返し」を避け、 -安定した成長可能なシステムが構築できます。 \ No newline at end of file diff --git a/oauth/IMPROVEMENT_PLAN.md b/oauth/IMPROVEMENT_PLAN.md deleted file mode 100644 index 8404ff0..0000000 --- a/oauth/IMPROVEMENT_PLAN.md +++ /dev/null @@ -1,448 +0,0 @@ -# OAuth_new 改善計画 - -## 現状分析 - -### 良い点 -- ✅ クリーンなアーキテクチャ(Hooks分離) -- ✅ 公式ライブラリ使用(@atproto/oauth-client-browser) -- ✅ 適切なエラーハンドリング -- ✅ 包括的なドキュメント -- ✅ 環境変数による設定外部化 - -### 問題点 -- ❌ パフォーマンス:毎回全データを並列取得 -- ❌ UX:ローディング状態が分かりにくい -- ❌ スケーラビリティ:データ量増加への対応不足 -- ❌ エラー詳細度:汎用的すぎるエラーメッセージ -- ❌ リアルタイム性:手動更新が必要 - -## 改善計画 - -### Phase 1: 安定性・パフォーマンス向上(優先度:高) - -#### 1.1 キャッシュシステム導入 -```javascript -// 新規ファイル: src/utils/cache.js -export class DataCache { - constructor(ttl = 30000) { // 30秒TTL - this.cache = new Map() - this.ttl = ttl - } - - get(key) { - const item = this.cache.get(key) - if (!item) return null - - if (Date.now() - item.timestamp > this.ttl) { - this.cache.delete(key) - return null - } - return item.data - } - - set(key, data) { - this.cache.set(key, { - data, - timestamp: Date.now() - }) - } - - invalidate(pattern) { - for (const key of this.cache.keys()) { - if (key.includes(pattern)) { - this.cache.delete(key) - } - } - } -} -``` - -#### 1.2 リトライ機能付きAPI -```javascript -// 修正: src/api/atproto.js -async function requestWithRetry(url, options = {}, maxRetries = 3) { - for (let i = 0; i < maxRetries; i++) { - try { - const response = await fetch(url, options) - if (!response.ok) { - throw new Error(`HTTP ${response.status}`) - } - return await response.json() - } catch (error) { - if (i === maxRetries - 1) throw error - - // 指数バックオフ - const delay = Math.min(1000 * Math.pow(2, i), 10000) - await new Promise(resolve => setTimeout(resolve, delay)) - } - } -} -``` - -#### 1.3 詳細なエラーハンドリング -```javascript -// 新規ファイル: src/utils/errorHandler.js -export class ATProtoError extends Error { - constructor(message, status, context) { - super(message) - this.status = status - this.context = context - this.timestamp = new Date().toISOString() - } -} - -export function getErrorMessage(error) { - if (error.status === 400) { - return 'アカウントまたはコレクションが見つかりません' - } else if (error.status === 429) { - return 'レート制限です。しばらく待ってから再試行してください' - } else if (error.status === 500) { - return 'サーバーエラーが発生しました' - } else if (error.message.includes('NetworkError')) { - return 'ネットワーク接続を確認してください' - } - return '予期しないエラーが発生しました' -} -``` - -### Phase 2: UX改善(優先度:中) - -#### 2.1 ローディング状態の改善 -```javascript -// 修正: src/components/RecordTabs.jsx -const LoadingSkeleton = ({ count = 3 }) => ( -
- {Array(count).fill(0).map((_, i) => ( -
-
-
-
-
-
-
- ))} -
-) - -// CSS追加 -.skeleton-item { - display: flex; - padding: 10px; - border: 1px solid #eee; - margin: 5px 0; -} -.skeleton-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} -@keyframes loading { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} -``` - -#### 2.2 インクリメンタルローディング -```javascript -// 修正: src/hooks/useAdminData.js -export function useAdminData() { - const [adminData, setAdminData] = useState({ - did: '', - profile: null, - records: [], - apiConfig: null - }) - const [langRecords, setLangRecords] = useState([]) - const [commentRecords, setCommentRecords] = useState([]) - const [loadingStates, setLoadingStates] = useState({ - admin: true, - lang: true, - comment: true - }) - - const loadAdminData = async () => { - try { - // 管理者データを最初に読み込み - setLoadingStates(prev => ({ ...prev, admin: true })) - const apiConfig = getApiConfig(`https://${env.pds}`) - const did = await atproto.getDid(env.pds, env.admin) - const profile = await atproto.getProfile(apiConfig.bsky, did) - - setAdminData({ did, profile, records: [], apiConfig }) - setLoadingStates(prev => ({ ...prev, admin: false })) - - // 基本レコードを読み込み - const records = await collections.getBase(apiConfig.pds, did, env.collection) - setAdminData(prev => ({ ...prev, records })) - - // lang/commentを並列で読み込み - const [lang, comment] = await Promise.all([ - collections.getLang(apiConfig.pds, did, env.collection) - .finally(() => setLoadingStates(prev => ({ ...prev, lang: false }))), - collections.getComment(apiConfig.pds, did, env.collection) - .finally(() => setLoadingStates(prev => ({ ...prev, comment: false }))) - ]) - - setLangRecords(lang) - setCommentRecords(comment) - } catch (err) { - // エラーハンドリング - } - } - - return { - adminData, - langRecords, - commentRecords, - loadingStates, - refresh: loadAdminData - } -} -``` - -### Phase 3: リアルタイム機能(優先度:中) - -#### 3.1 WebSocket統合 -```javascript -// 新規ファイル: src/hooks/useRealtimeUpdates.js -import { useState, useEffect, useRef } from 'react' - -export function useRealtimeUpdates(collection, onNewRecord) { - const [connected, setConnected] = useState(false) - const wsRef = useRef(null) - const reconnectTimeoutRef = useRef(null) - - const connect = () => { - try { - wsRef.current = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe') - - wsRef.current.onopen = () => { - setConnected(true) - console.log('WebSocket connected') - - // Subscribe to specific collection - wsRef.current.send(JSON.stringify({ - type: 'subscribe', - collections: [collection] - })) - } - - wsRef.current.onmessage = (event) => { - try { - const data = JSON.parse(event.data) - if (data.collection === collection && data.commit?.operation === 'create') { - onNewRecord(data.commit.record) - } - } catch (err) { - console.warn('Failed to parse WebSocket message:', err) - } - } - - wsRef.current.onclose = () => { - setConnected(false) - // Auto-reconnect after 5 seconds - reconnectTimeoutRef.current = setTimeout(connect, 5000) - } - - wsRef.current.onerror = (error) => { - console.error('WebSocket error:', error) - setConnected(false) - } - } catch (err) { - console.error('Failed to connect WebSocket:', err) - } - } - - useEffect(() => { - connect() - - return () => { - if (wsRef.current) { - wsRef.current.close() - } - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current) - } - } - }, [collection]) - - return { connected } -} -``` - -#### 3.2 オプティミスティック更新 -```javascript -// 修正: src/components/CommentForm.jsx -const handleSubmit = async (e) => { - e.preventDefault() - if (!text.trim() || !url.trim()) return - - setLoading(true) - setError(null) - - // オプティミスティック更新用の仮レコード - const optimisticRecord = { - uri: `temp-${Date.now()}`, - cid: 'temp', - value: { - $type: env.collection, - url: url.trim(), - comments: [{ - url: url.trim(), - text: text.trim(), - author: { - did: user.did, - handle: user.handle, - displayName: user.displayName, - avatar: user.avatar - }, - createdAt: new Date().toISOString() - }], - createdAt: new Date().toISOString() - } - } - - // UIに即座に反映 - if (onOptimisticUpdate) { - onOptimisticUpdate(optimisticRecord) - } - - try { - const record = { - repo: user.did, - collection: env.collection, - rkey: `comment-${Date.now()}`, - record: optimisticRecord.value - } - - await atproto.putRecord(null, record, agent) - - // 成功時はフォームをクリア - setText('') - setUrl('') - - if (onCommentPosted) { - onCommentPosted() - } - } catch (err) { - // 失敗時はオプティミスティック更新を取り消し - if (onOptimisticRevert) { - onOptimisticRevert(optimisticRecord.uri) - } - setError(err.message) - } finally { - setLoading(false) - } -} -``` - -### Phase 4: TypeScript化・テスト(優先度:低) - -#### 4.1 TypeScript移行 -```typescript -// 新規ファイル: src/types/atproto.ts -export interface ATProtoRecord { - uri: string - cid: string - value: { - $type: string - createdAt: string - [key: string]: any - } -} - -export interface CommentRecord extends ATProtoRecord { - value: { - $type: string - url: string - comments: Comment[] - createdAt: string - } -} - -export interface Comment { - url: string - text: string - author: Author - createdAt: string -} - -export interface Author { - did: string - handle: string - displayName?: string - avatar?: string -} -``` - -#### 4.2 テスト環境 -```javascript -// 新規ファイル: src/tests/hooks/useAdminData.test.js -import { renderHook, waitFor } from '@testing-library/react' -import { useAdminData } from '../../hooks/useAdminData' - -// Mock API -jest.mock('../../api/atproto', () => ({ - atproto: { - getDid: jest.fn(), - getProfile: jest.fn() - }, - collections: { - getBase: jest.fn(), - getLang: jest.fn(), - getComment: jest.fn() - } -})) - -describe('useAdminData', () => { - test('loads admin data successfully', async () => { - const { result } = renderHook(() => useAdminData()) - - await waitFor(() => { - expect(result.current.adminData.did).toBeTruthy() - }) - }) -}) -``` - -## 実装優先順位 - -### 今すぐ実装すべき(Phase 1) -1. **エラーハンドリング改善** - 1日で実装可能 -2. **キャッシュシステム** - 2日で実装可能 -3. **リトライ機能** - 1日で実装可能 - -### 短期実装(1週間以内) -1. **ローディングスケルトン** - UX大幅改善 -2. **インクリメンタルローディング** - パフォーマンス向上 - -### 中期実装(1ヶ月以内) -1. **WebSocketリアルタイム更新** - 新機能 -2. **オプティミスティック更新** - UX向上 - -### 長期実装(必要に応じて) -1. **TypeScript化** - 保守性向上 -2. **テスト追加** - 品質保証 - -## 注意事項 - -### 既存機能への影響 -- すべての改善は後方互換性を保つ -- 段階的実装で破綻リスクを最小化 -- 各Phase完了後に動作確認 - -### パフォーマンス指標 -- 初期表示時間: 現在3秒 → 目標1秒 -- キャッシュヒット率: 目標70%以上 -- エラー率: 現在10% → 目標2%以下 - -### ユーザビリティ指標 -- ローディング状態の可視化 -- エラーメッセージの分かりやすさ -- リアルタイム更新の応答性 - -この改善計画により、oauth_newは./oauthの問題を回避しながら、 -より安定した高性能なシステムに進化できます。 \ No newline at end of file diff --git a/oauth/OAUTH_FIX.md b/oauth/OAUTH_FIX.md deleted file mode 100644 index 82951ac..0000000 --- a/oauth/OAUTH_FIX.md +++ /dev/null @@ -1,81 +0,0 @@ -# OAuth認証の修正案 - -## 現在の問題 - -1. **スコープエラー**: `Missing required scope: transition:generic` - - OAuth認証時に必要なスコープが不足している - - ✅ 修正済み: `scope: 'atproto transition:generic'` に変更 - -2. **401エラー**: PDSへの直接アクセス - - `https://shiitake.us-east.host.bsky.network/xrpc/app.bsky.actor.getProfile` で401エラー - - 原因: 個人のPDSに直接アクセスしているが、これは認証が必要 - - 解決策: 公開APIエンドポイント(`https://public.api.bsky.app`)を使用すべき - -3. **セッション保存の問題**: handleが`@unknown`になる - - OAuth認証後にセッションが正しく保存されていない - - ✅ 修正済み: Agentの作成方法を修正 - -## 修正が必要な箇所 - -### 1. avatarFetcher.js の修正 -個人のPDSではなく、公開APIを使用するように修正: - -```javascript -// 現在の問題のあるコード -const response = await fetch(`${apiConfig.bsky}/xrpc/app.bsky.actor.getProfile?actor=${did}`) - -// 修正案 -// PDSに関係なく、常に公開APIを使用 -const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`) -``` - -### 2. セッション復元の改善 -OAuth認証後のコールバック処理で、セッションが正しく復元されていない可能性がある。 - -```javascript -// restoreSession メソッドの改善 -async restoreSession() { - // Try both clients - for (const [name, client] of Object.entries(this.clients)) { - if (!client) continue - - const result = await client.init() - if (result?.session) { - // セッション処理を確実に行う - this.agent = new Agent(result.session) - const sessionInfo = await this.processSession(result.session) - - // セッション情報をログに出力(デバッグ用) - logger.log('Session restored:', { name, sessionInfo }) - - return sessionInfo - } - } - return null -} -``` - -## 根本的な問題 - -1. **PDSアクセスの誤解** - - `app.bsky.actor.getProfile` は公開API(認証不要) - - 個人のPDSサーバーに直接アクセスする必要はない - - 常に `https://public.api.bsky.app` を使用すべき - -2. **OAuth Clientの初期化タイミング** - - コールバック時に両方のクライアント(bsky, syu)を試す必要がある - - どちらのPDSでログインしたか分からないため - -## 推奨される修正手順 - -1. **即座の修正**(401エラー解決) - - `avatarFetcher.js` で公開APIを使用 - - `getProfile` 呼び出しをすべて公開APIに変更 - -2. **セッション管理の改善** - - OAuth認証後のセッション復元を確実に - - エラーハンドリングの強化 - -3. **デバッグ情報の追加** - - セッション復元時のログ追加 - - どのOAuthクライアントが使用されたか確認 \ No newline at end of file diff --git a/oauth/PHASE1_QUICK_FIXES.md b/oauth/PHASE1_QUICK_FIXES.md deleted file mode 100644 index c68f3b5..0000000 --- a/oauth/PHASE1_QUICK_FIXES.md +++ /dev/null @@ -1,601 +0,0 @@ -# Phase 1: 即座実装可能な修正 - -## 1. エラーハンドリング強化(30分で実装) - -### ファイル作成: `src/utils/errorHandler.js` -```javascript -export class ATProtoError extends Error { - constructor(message, status, context) { - super(message) - this.status = status - this.context = context - this.timestamp = new Date().toISOString() - } -} - -export function getErrorMessage(error) { - if (!error) return '不明なエラー' - - if (error.status === 400) { - return 'アカウントまたはレコードが見つかりません' - } else if (error.status === 401) { - return '認証が必要です。ログインしてください' - } else if (error.status === 403) { - return 'アクセス権限がありません' - } else if (error.status === 429) { - return 'アクセスが集中しています。しばらく待ってから再試行してください' - } else if (error.status === 500) { - return 'サーバーでエラーが発生しました' - } else if (error.message?.includes('fetch')) { - return 'ネットワーク接続を確認してください' - } else if (error.message?.includes('timeout')) { - return 'タイムアウトしました。再試行してください' - } - - return `エラーが発生しました: ${error.message || '不明'}` -} - -export function logError(error, context = 'Unknown') { - const errorInfo = { - context, - message: error.message, - status: error.status, - timestamp: new Date().toISOString(), - url: window.location.href - } - - console.error(`[ATProto Error] ${context}:`, errorInfo) - - // 本番環境では外部ログサービスに送信することも可能 - // if (import.meta.env.PROD) { - // sendToLogService(errorInfo) - // } -} -``` - -### 修正: `src/api/atproto.js` のrequest関数 -```javascript -import { ATProtoError, logError } from '../utils/errorHandler.js' - -async function request(url, options = {}) { - try { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 15000) // 15秒タイムアウト - - const response = await fetch(url, { - ...options, - signal: controller.signal - }) - - clearTimeout(timeoutId) - - if (!response.ok) { - throw new ATProtoError( - `Request failed: ${response.statusText}`, - response.status, - { url, method: options.method || 'GET' } - ) - } - - return await response.json() - } catch (error) { - if (error.name === 'AbortError') { - const timeoutError = new ATProtoError( - 'リクエストがタイムアウトしました', - 408, - { url } - ) - logError(timeoutError, 'Request Timeout') - throw timeoutError - } - - if (error instanceof ATProtoError) { - logError(error, 'API Request') - throw error - } - - // ネットワークエラーなど - const networkError = new ATProtoError( - 'ネットワークエラーが発生しました', - 0, - { url, originalError: error.message } - ) - logError(networkError, 'Network Error') - throw networkError - } -} -``` - -### 修正: `src/hooks/useAdminData.js` -```javascript -import { getErrorMessage, logError } from '../utils/errorHandler.js' - -export function useAdminData() { - // 既存のstate... - const [error, setError] = useState(null) - const [retryCount, setRetryCount] = useState(0) - - const loadAdminData = async () => { - try { - setLoading(true) - setError(null) - - const apiConfig = getApiConfig(`https://${env.pds}`) - const did = await atproto.getDid(env.pds, env.admin) - const profile = await atproto.getProfile(apiConfig.bsky, did) - - // Load all data in parallel - const [records, lang, comment] = await Promise.all([ - collections.getBase(apiConfig.pds, did, env.collection), - collections.getLang(apiConfig.pds, did, env.collection), - collections.getComment(apiConfig.pds, did, env.collection) - ]) - - setAdminData({ did, profile, records, apiConfig }) - setLangRecords(lang) - setCommentRecords(comment) - setRetryCount(0) // 成功時はリトライカウントをリセット - } catch (err) { - logError(err, 'useAdminData.loadAdminData') - setError(getErrorMessage(err)) - - // 自動リトライ(最大3回) - if (retryCount < 3) { - setTimeout(() => { - setRetryCount(prev => prev + 1) - loadAdminData() - }, Math.pow(2, retryCount) * 1000) // 1s, 2s, 4s - } - } finally { - setLoading(false) - } - } - - return { - adminData, - langRecords, - commentRecords, - loading, - error, - retryCount, - refresh: loadAdminData - } -} -``` - -## 2. シンプルキャッシュ(15分で実装) - -### ファイル作成: `src/utils/cache.js` -```javascript -class SimpleCache { - constructor(ttl = 30000) { // 30秒TTL - this.cache = new Map() - this.ttl = ttl - } - - generateKey(...parts) { - return parts.filter(Boolean).join(':') - } - - get(key) { - const item = this.cache.get(key) - if (!item) return null - - if (Date.now() - item.timestamp > this.ttl) { - this.cache.delete(key) - return null - } - - console.log(`Cache hit: ${key}`) - return item.data - } - - set(key, data) { - this.cache.set(key, { - data, - timestamp: Date.now() - }) - console.log(`Cache set: ${key}`) - } - - clear() { - this.cache.clear() - console.log('Cache cleared') - } - - invalidatePattern(pattern) { - let deletedCount = 0 - for (const key of this.cache.keys()) { - if (key.includes(pattern)) { - this.cache.delete(key) - deletedCount++ - } - } - console.log(`Cache invalidated: ${pattern} (${deletedCount} items)`) - } - - getStats() { - return { - size: this.cache.size, - keys: Array.from(this.cache.keys()) - } - } -} - -export const dataCache = new SimpleCache() - -// デバッグ用:グローバルからアクセス可能にする -if (import.meta.env.DEV) { - window.dataCache = dataCache -} -``` - -### 修正: `src/api/atproto.js` のcollections -```javascript -import { dataCache } from '../utils/cache.js' - -export const collections = { - async getBase(pds, repo, collection, limit = 10) { - const cacheKey = dataCache.generateKey('base', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, collection, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getLang(pds, repo, collection, limit = 10) { - const cacheKey = dataCache.generateKey('lang', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getComment(pds, repo, collection, limit = 10) { - const cacheKey = dataCache.generateKey('comment', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getChat(pds, repo, collection, limit = 10) { - const cacheKey = dataCache.generateKey('chat', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getUserList(pds, repo, collection, limit = 100) { - const cacheKey = dataCache.generateKey('userlist', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, `${collection}.user`, limit) - dataCache.set(cacheKey, data) - return data - }, - - async getUserComments(pds, repo, collection, limit = 10) { - const cacheKey = dataCache.generateKey('usercomments', pds, repo, collection, limit) - const cached = dataCache.get(cacheKey) - if (cached) return cached - - const data = await atproto.getRecords(pds, repo, collection, limit) - dataCache.set(cacheKey, data) - return data - }, - - // 投稿後にキャッシュを無効化 - invalidateCache(collection) { - dataCache.invalidatePattern(collection) - } -} -``` - -### 修正: `src/components/CommentForm.jsx` にキャッシュクリア追加 -```javascript -// handleSubmit内の成功時処理に追加 -try { - await atproto.putRecord(null, record, agent) - - // キャッシュを無効化 - collections.invalidateCache(env.collection) - - // Clear form - setText('') - setUrl('') - - // Notify parent component - if (onCommentPosted) { - onCommentPosted() - } -} catch (err) { - setError(err.message) -} -``` - -## 3. ローディング改善(20分で実装) - -### ファイル作成: `src/components/LoadingSkeleton.jsx` -```javascript -import React from 'react' - -export default function LoadingSkeleton({ count = 3, showTitle = false }) { - return ( -
- {showTitle && ( -
-
-
- )} - - {Array(count).fill(0).map((_, i) => ( -
-
-
-
-
-
-
-
-
- ))} - - -
- ) -} -``` - -### 修正: `src/components/RecordTabs.jsx` -```javascript -import LoadingSkeleton from './LoadingSkeleton.jsx' - -export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, apiConfig, pageContext }) { - const [activeTab, setActiveTab] = useState('lang') - - // ... 既存のロジック - - return ( -
-
- - - - -
- -
- {activeTab === 'lang' && ( - !langRecords ? ( - - ) : ( - - ) - )} - - {activeTab === 'comment' && ( - !commentRecords ? ( - - ) : ( - - ) - )} - - {activeTab === 'collection' && ( - !chatRecords ? ( - - ) : ( - - ) - )} - - {activeTab === 'users' && ( - !userComments ? ( - - ) : ( - - ) - )} -
- - {/* 既存のstyle... */} -
- ) -} -``` - -### 修正: `src/App.jsx` にエラー表示改善 -```javascript -import { getErrorMessage } from './utils/errorHandler.js' - -export default function App() { - const { user, agent, loading: authLoading, login, logout } = useAuth() - const { adminData, langRecords, commentRecords, loading: dataLoading, error, retryCount, refresh: refreshAdminData } = useAdminData() - const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData) - const pageContext = usePageContext() - - // ... 既存のロジック - - if (error) { - return ( -
-

ATProto OAuth Demo

-
-

エラー: {error}

- {retryCount > 0 && ( -

自動リトライ中... ({retryCount}/3)

- )} -
- -
- ) - } - - // ... 既存のレンダリング -} -``` - -## 実装チェックリスト - -### ✅ Phase 1A: エラーハンドリング(30分) -- [ ] `src/utils/errorHandler.js` 作成 -- [ ] `src/api/atproto.js` の `request` 関数修正 -- [ ] `src/hooks/useAdminData.js` エラーハンドリング追加 -- [ ] `src/App.jsx` エラー表示改善 - -### ✅ Phase 1B: キャッシュ(15分) -- [ ] `src/utils/cache.js` 作成 -- [ ] `src/api/atproto.js` の `collections` にキャッシュ追加 -- [ ] `src/components/CommentForm.jsx` にキャッシュクリア追加 - -### ✅ Phase 1C: ローディングUI(20分) -- [ ] `src/components/LoadingSkeleton.jsx` 作成 -- [ ] `src/components/RecordTabs.jsx` にローディング表示追加 - -### テスト -- [ ] エラー状態でも適切にメッセージが表示される -- [ ] キャッシュがコンソールログで確認できる -- [ ] ローディング中にスケルトンが表示される -- [ ] 投稿後にキャッシュがクリアされる - -**実装時間目安**: 65分(エラーハンドリング30分 + キャッシュ15分 + ローディング20分) - -これらの修正により、oauth_newは./oauthで頻発している問題を回避し、 -より安定したユーザー体験を提供できます。 \ No newline at end of file diff --git a/oauth/PROGRESS.md b/oauth/PROGRESS.md deleted file mode 100644 index 7720411..0000000 --- a/oauth/PROGRESS.md +++ /dev/null @@ -1,120 +0,0 @@ -# OAuth Comment System 開発進捗 - 2025-06-18 - -## 今日完了した項目 - -### ✅ UI改善とスタイリング -1. **ヘッダータイトル削除**: "ai.log"タイトルを削除 -2. **ログインボタンアイコン化**: テキストからBlueskyアイコン `` に変更 -3. **Ask AIボタン削除**: 完全に削除 -4. **Testボタン移動**: ページ下部に移動、テキストを小文字に変更 -5. **検索バーレイアウト適用**: 認証セクションに検索バーUIパターンを適用 -6. **ボーダー削除**: 複数の要素からborder-top, border-bottom削除 -7. **ヘッダースペーシング修正**: 左側の余白問題を解決 -8. **CSS競合解決**: クラス名に`oauth-`プレフィックス追加でailogサイトとの競合回避 -9. **パディング統一**: `padding: 20px 0` に統一(デスクトップ・モバイル共通) - -### ✅ 機能実装 -1. **テスト用UI作成**: OAuth認証不要のputRecord機能実装 -2. **JSONビューワー追加**: コメント表示にshow/hideボタン追加 -3. **削除機能追加**: OAuth認証ユーザー用のdeleteボタン実装 -4. **動的アバター取得**: 壊れたアバターURL対応のフォールバック機能 -5. **ブラウザ動作確認**: 全機能の動作テスト完了 - -### ✅ 技術的解決 -1. **DID処理改善**: テスト用の偽DITエラー修正 -2. **Handle処理修正**: 自動`.bsky.social`追加削除 -3. **セッション管理**: createSession機能の修正 -4. **アバターキャッシュ**: 動的取得とキャッシュ機能実装 - -## 現在の技術構成 - -### フロントエンド -- **React + Vite**: モダンなSPA構成 -- **ATProto OAuth**: Bluesky認証システム -- **アバター管理**: 動的取得・フォールバック・キャッシュ -- **レスポンシブデザイン**: モバイル・デスクトップ対応 - -### バックエンド連携 -- **ATProto API**: PDS通信 -- **Collection管理**: `ai.syui.log.chat.comment`等のレコード操作 -- **DID解決**: Handle → DID → PDS → Profile取得 - -### CSS設計 -- **Prefix命名**: `oauth-`で競合回避 -- **統一パディング**: `20px 0`でレイアウト統一 -- **ailogスタイル継承**: 親サイトとの一貫性保持 - -## ファイル構成 - -``` -oauth_new/ -├── src/ -│ ├── App.jsx # メインアプリケーション -│ ├── App.css # 統一スタイル(oauth-プレフィックス) -│ ├── components/ -│ │ ├── AuthButton.jsx # Blueskyアイコン認証ボタン -│ │ ├── CommentForm.jsx # コメント投稿フォーム -│ │ ├── CommentList.jsx # コメント一覧表示 -│ │ └── TestUI.jsx # テスト用UI -│ └── utils/ -│ └── avatarFetcher.js # アバター動的取得 -├── dist/ # ビルド成果物 -├── build-minimal.js # 最小化ビルドスクリプト -└── PROGRESS.md # 本ファイル -``` - -## 残存課題・継続開発項目 - -### 🔄 現在進行中 -- 特になし(基本機能完成) - -### 📋 今後の拡張予定 -1. **AI連携強化** - - ai.gptとの統合 - - AIコメント自動生成 - - 心理分析機能統合 - -2. **パフォーマンス最適化** - - バンドルサイズ削減(現在1.2MB) - - 動的インポート実装 - - キャッシュ戦略改善 - -3. **機能拡張** - - リアルタイム更新 - - 通知システム - - モデレーション機能 - - 多言語対応 - -4. **ai.log統合** - - 静的ブログジェネレーター連携 - - 記事別コメント管理 - - SEO最適化 - -### 🎯 次回セッション予定 -1. ai.gpt連携の詳細設計 -2. パフォーマンス最適化 -3. ai.log本体との統合テスト - -## 技術メモ - -### 重要な解決方法 -- **CSS競合**: `oauth-`プレフィックスで名前空間分離 -- **アバター問題**: 3段階フォールバック(record → fresh fetch → fallback) -- **認証フロー**: session管理とDID-based認証 -- **レスポンシブ**: 統一パディングでシンプル化 - -### 設定ファイル連携 -- `./my-blog/config.toml`: ブログ設定 -- `./oauth/.env.production`: OAuth設定 -- `~/.config/syui/ai/log/config.json`: システム設定 - -## 成果物 - -✅ **完全に動作するOAuthコメントシステム** -- ATProto認証 -- コメント投稿・表示・削除 -- アバター表示 -- JSON詳細表示 -- テスト機能 -- レスポンシブデザイン -- ailogサイトとの統合準備完了 \ No newline at end of file diff --git a/oauth/README.md b/oauth/README.md deleted file mode 100644 index f369473..0000000 --- a/oauth/README.md +++ /dev/null @@ -1,222 +0,0 @@ -# ATProto OAuth Comment System - -ATProtocol(Bluesky)のOAuth認証を使用したコメントシステムです。 - -## プロジェクト概要 - -このプロジェクトは、ATProtocolネットワーク上のコメントとlangレコードを表示するWebアプリケーションです。 -- 標準的なOAuth認証画面を使用 -- タブ切り替えでレコード表示 -- ページコンテキストに応じたフィルタリング - -## ファイル構成 - -``` -src/ -├── config/ -│ └── env.js # 環境変数の一元管理 -├── utils/ -│ └── pds.js # PDS判定・API設定ユーティリティ -├── api/ -│ └── atproto.js # ATProto API クライアント -├── hooks/ -│ ├── useAuth.js # OAuth認証フック -│ ├── useAdminData.js # 管理者データ取得フック -│ └── usePageContext.js # ページ判定フック -├── services/ -│ └── oauth.js # OAuth認証サービス -├── components/ -│ ├── AuthButton.jsx # ログイン/ログアウトボタン -│ ├── RecordTabs.jsx # Lang/Commentタブ切り替え -│ ├── RecordList.jsx # レコード表示リスト -│ ├── UserLookup.jsx # ユーザー検索(未使用) -│ └── OAuthCallback.jsx # OAuth コールバック処理 -└── App.jsx # メインアプリケーション -``` - -## 環境設定 - -### .env ファイル - -```bash -VITE_ADMIN=ai.syui.ai # 管理者ハンドル -VITE_PDS=syu.is # デフォルトPDS -VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"] # syu.is系ハンドルリスト -VITE_COLLECTION=ai.syui.log # ベースコレクション -VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json # OAuth クライアントID -VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback # OAuth リダイレクトURI -``` - -### 必要な依存関係 - -```json -{ - "dependencies": { - "@atproto/api": "^0.15.12", - "@atproto/oauth-client-browser": "^0.3.19", - "react": "^18.2.0", - "react-dom": "^18.2.0" - } -} -``` - -## 主要機能 - -### 1. OAuth認証システム - -**実装場所**: `src/services/oauth.js` - -- `@atproto/oauth-client-browser`を使用した標準OAuth実装 -- bsky.social と syu.is 両方のPDSに対応 -- セッション自動復元機能 - -**重要**: ATProtoのセッション管理は複雑なため、公式ライブラリの使用が必須です。 - -### 2. PDS判定システム - -**実装場所**: `src/utils/pds.js` - -```javascript -// ハンドル判定ロジック -isSyuIsHandle(handle) → boolean -// PDS設定取得 -getApiConfig(pds) → { pds, bsky, plc, web } -``` - -環境変数`VITE_HANDLE_LIST`と`VITE_PDS`を基に自動判定します。 - -### 3. コレクション取得システム - -**実装場所**: `src/api/atproto.js` - -```javascript -// 基本コレクション -collections.getBase(pds, repo, collection) -// lang コレクション(翻訳系) -collections.getLang(pds, repo, collection) // → {collection}.chat.lang -// comment コレクション(コメント系) -collections.getComment(pds, repo, collection) // → {collection}.chat.comment -``` - -### 4. ページコンテキスト判定 - -**実装場所**: `src/hooks/usePageContext.js` - -```javascript -// URL解析結果 -{ - isTopPage: boolean, // トップページかどうか - rkey: string | null, // 個別ページのrkey(/posts/xxx → xxx) - url: string // 現在のURL -} -``` - -## 表示ロジック - -### フィルタリング - -1. **トップページ**: 最新3件を表示 -2. **個別ページ**: `record.value.post.url`の rkey が現在ページと一致するもののみ表示 - -### タブ切り替え - -- Lang Records: `{collection}.chat.lang` -- Comment Records: `{collection}.chat.comment` - -## 開発・デバッグ - -### 起動コマンド - -```bash -npm install -npm run dev # 開発サーバー -npm run build # プロダクションビルド -``` - -### OAuth デバッグ - -1. **ローカル開発**: 自動的にloopback clientが使用される -2. **本番環境**: `client-metadata.json`が必要 - -```json -// public/client-metadata.json -{ - "client_id": "https://syui.ai/client-metadata.json", - "client_name": "ATProto Comment System", - "redirect_uris": ["https://syui.ai/oauth/callback"], - "scope": "atproto", - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "token_endpoint_auth_method": "none", - "application_type": "web", - "dpop_bound_access_tokens": true -} -``` - -### よくある問題 - -1. **セッションが保存されない** - - `@atproto/oauth-client-browser`のバージョン確認 - - IndexedDBの確認(ブラウザの開発者ツール) - -2. **PDS判定が正しく動作しない** - - `VITE_HANDLE_LIST`の JSON 形式を確認 - - 環境変数の読み込み確認 - -3. **レコードが表示されない** - - 管理者アカウントの DID 解決確認 - - コレクション名の確認(`{base}.chat.lang`, `{base}.chat.comment`) - -## API エンドポイント - -### 使用しているATProto API - -1. **com.atproto.repo.describeRepo** - - ハンドル → DID, PDS解決 - -2. **app.bsky.actor.getProfile** - - プロフィール情報取得 - -3. **com.atproto.repo.listRecords** - - コレクションレコード取得 - -## セキュリティ - -- OAuth 2.1 + PKCE による認証 -- DPoP (Demonstration of Proof of Possession) 対応 -- セッション情報はブラウザのIndexedDBに暗号化保存 - -## 今後の拡張可能性 - -1. **コメント投稿機能** - - 認証済みユーザーによるコメント作成 - - `com.atproto.repo.putRecord` API使用 - -2. **リアルタイム更新** - - Jetstream WebSocket 接続 - - 新しいレコードの自動表示 - -3. **マルチPDS対応** - - より多くのPDSへの対応 - - 動的PDS判定の改善 - -## トラブルシューティング - -### ログ確認 -ブラウザの開発者ツールでコンソールログを確認してください。主要なエラーは以下の通りです: - -- `OAuth initialization failed`: OAuth設定の問題 -- `Failed to load admin data`: API アクセスエラー -- `Auth check failed`: セッション復元エラー - -### 環境変数確認 -```javascript -// 開発者ツールのコンソールで確認 -console.log(import.meta.env) -``` - -## 参考資料 - -- [ATProto OAuth Guide](https://github.com/bluesky-social/atproto/blob/main/packages/api/OAUTH.md) -- [BrowserOAuthClient Documentation](https://github.com/bluesky-social/atproto/tree/main/packages/oauth-client-browser) -- [ATProto API Reference](https://docs.bsky.app/docs/advanced-guides/atproto-api) \ No newline at end of file diff --git a/oauth/cleanup-deployments.yml b/oauth/cleanup-deployments.yml deleted file mode 100644 index b97feff..0000000 --- a/oauth/cleanup-deployments.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Cleanup Old Deployments - -on: - workflow_run: - workflows: ["Deploy to Cloudflare Pages"] - types: - - completed - workflow_dispatch: - -env: - KEEP_DEPLOYMENTS: 5 # 保持するデプロイメント数 - -jobs: - cleanup: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} - - steps: - - name: Cleanup old deployments - run: | - # Get all deployments - DEPLOYMENTS=$(curl -s -X GET \ - "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \ - -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type: application/json") - - # Extract deployment IDs (skip the latest N deployments) - DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id") - - # Delete old deployments - for ID in $DEPLOYMENT_IDS; do - echo "Deleting deployment: $ID" - curl -s -X DELETE \ - "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \ - -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type: application/json" - echo "Deleted deployment: $ID" - sleep 1 # Rate limiting - done - - echo "Cleanup completed!" \ No newline at end of file