cleanup docs

This commit is contained in:
2025-06-21 00:07:22 +09:00
parent eef1fdad38
commit 6be024864d
15 changed files with 0 additions and 3450 deletions

View File

@ -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"
}
```
## イベント通信
blogask-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 が無効化される

View File

@ -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
### `<Avatar>`
Basic avatar component with loading states and fallbacks.
```jsx
<Avatar
record={record}
handle="user.handle"
did="did:plc:xxx"
size={40}
showFallback={true}
onLoad={() => console.log('loaded')}
onError={(err) => console.log('error', err)}
/>
```
### `<AvatarWithCard>`
Avatar with hover card showing user information.
```jsx
<AvatarWithCard
record={record}
displayName="User Name"
apiConfig={apiConfig}
size={60}
/>
```
### `<AvatarList>`
Display multiple avatars with overlap effect.
```jsx
<AvatarList
users={userArray}
maxDisplay={5}
size={30}
/>
```
## 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 && (
<img src={record.value.author.avatar} alt="avatar" className="avatar" />
)}
// New approach
<Avatar
record={record}
handle={record.value.author?.handle}
did={record.value.author?.did}
size={40}
showFallback={true}
/>
```
## 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

View File

@ -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 (
<div className="user-profile">
<Avatar
handle={user.handle}
did={user.did}
size={80}
alt={`${user.displayName}'s avatar`}
/>
<h3>{user.displayName}</h3>
</div>
)
}
```
### Avatar from Record Data
```jsx
function CommentItem({ record }) {
return (
<div className="comment">
<Avatar
record={record}
size={40}
showFallback={true}
/>
<div className="comment-content">
<strong>{record.value.author.displayName}</strong>
<p>{record.value.text}</p>
</div>
</div>
)
}
```
### Avatar with Hover Card
```jsx
import { AvatarWithCard } from './components/Avatar.jsx'
function UserList({ users, apiConfig }) {
return (
<div className="user-list">
{users.map(user => (
<AvatarWithCard
key={user.handle}
handle={user.handle}
did={user.did}
displayName={user.displayName}
apiConfig={apiConfig}
size={50}
/>
))}
</div>
)
}
```
## 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 <div>Loading team...</div>
return (
<div className="team">
{team.members.map(member => (
<img
key={member.handle}
src={avatars.get(member.handle) || '/default-avatar.png'}
alt={member.displayName}
/>
))}
</div>
)
}
```
### 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 (
<div className="refreshable-avatar">
<Avatar
key={key}
handle={handle}
did={did}
size={60}
/>
<button onClick={handleRefresh}>
Refresh Avatar
</button>
</div>
)
}
```
### Avatar List with Overflow
```jsx
import { AvatarList } from './components/Avatar.jsx'
function ParticipantsList({ participants, maxVisible = 5 }) {
return (
<div className="participants">
<h4>Participants ({participants.length})</h4>
<AvatarList
users={participants}
maxDisplay={maxVisible}
size={32}
/>
{participants.length > maxVisible && (
<span className="overflow-text">
and {participants.length - maxVisible} more...
</span>
)}
</div>
)
}
```
## 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 (
<img
src={fallbackSrc}
alt="Fallback avatar"
className="avatar"
onError={() => setHasError(false)} // Reset on fallback error
/>
)
}
return (
<Avatar
handle={handle}
did={did}
onError={handleError}
showFallback={!hasError}
/>
)
}
```
### 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 <div className="avatar-loading-spinner">Loading...</div>
}
if (error) {
return <div className="avatar-error">Failed to load avatar</div>
}
if (!avatar) {
return <div className="avatar-placeholder">No avatar</div>
}
return <img src={avatar} alt="Avatar" className="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 (
<div className="user-card">
{isVisible && (
<Avatar handle={user.handle} did={user.did} />
)}
<h4>{user.displayName}</h4>
</div>
)
}
```
### 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 (
<div ref={ref}>
{isVisible ? (
<Avatar handle={handle} did={did} {...props} />
) : (
<div className="avatar-placeholder" style={{
width: props.size || 40,
height: props.size || 40
}} />
)}
</div>
)
}
```
## 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 (
<div className="cache-stats">
<h4>Avatar Cache Stats</h4>
<p>Cached avatars: {stats.totalCached}</p>
<p>Cache hit rate: {stats.hitRate}%</p>
<p>Cache hits: {stats.cacheHits}</p>
<p>Cache misses: {stats.cacheMisses}</p>
<button onClick={handleCleanup}>
Clean Expired Cache
</button>
</div>
)
}
```
## 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 (
<div
className="avatar-mock"
style={{
width: size,
height: size,
borderRadius: '50%',
backgroundColor: '#e1e1e1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: size * 0.4,
color: '#666'
}}
>
{initial}
</div>
)
}
// 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.

View File

@ -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

View File

@ -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!"

View File

@ -1,178 +0,0 @@
# 本番環境デプロイメント手順
## 本番環境用の調整
### 1. テスト機能の削除・無効化
本番環境では以下の調整が必要です:
#### A. TestUI コンポーネントの削除
```jsx
// src/App.jsx から以下を削除/コメントアウト
import TestUI from './components/TestUI.jsx'
const [showTestUI, setShowTestUI] = useState(false)
// ボトムセクションからTestUIを削除
{showTestUI && (
<TestUI />
)}
<button
onClick={() => setShowTestUI(!showTestUI)}
className={`btn ${showTestUI ? 'btn-danger' : 'btn-outline'} btn-sm`}
>
{showTestUI ? 'close test' : 'test'}
</button>
```
#### 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
<!-- ailog のテンプレートに追加 -->
{{ 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用に調整必要
- バンドルサイズが大きいため今後最適化検討

View File

@ -1,334 +0,0 @@
# 開発ガイド
## 設計思想
このプロジェクトは以下の原則に基づいて設計されています:
### 1. 環境変数による設定の外部化
- ハードコードを避け、設定は全て環境変数で管理
- `src/config/env.js` で一元管理
### 2. PDSPersonal 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へのセッション保存
- トークンの自動更新
- DPoPDemonstration 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 <div>{data.map(...)}</div>
}
// ✅ Good: Hooksでロジック分離
function MyComponent() {
const { data, loading, error } = useMyData()
if (loading) return <Loading />
if (error) return <Error />
return <div>{data.map(...)}</div>
}
```
## デバッグ手法
### 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要件
- [ ] CSPContent 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])
}
```

View File

@ -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 && (
<div className="test-section">
<TestUI />
</div>
)}
// Testボタン表示制御
{ENABLE_TEST_UI && (
<div className="bottom-actions">
<button onClick={() => setShowTestUI(!showTestUI)}>
{showTestUI ? 'close test' : 'test'}
</button>
</div>
)}
```
### 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出力なし確認

View File

@ -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 (
<div className="loading-skeleton">
{Array(count).fill(0).map((_, i) => (
<div key={i} className="skeleton-item">
<div className="skeleton-avatar"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line short"></div>
<div className="skeleton-line shorter"></div>
</div>
</div>
))}
<style jsx>{`
.loading-skeleton {
padding: 10px;
}
.skeleton-item {
display: flex;
padding: 15px;
border: 1px solid #eee;
margin: 10px 0;
border-radius: 8px;
background: #fafafa;
}
.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;
margin-right: 10px;
flex-shrink: 0;
}
.skeleton-content {
flex: 1;
}
.skeleton-line {
height: 12px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 8px;
border-radius: 4px;
}
.skeleton-line.short {
width: 70%;
}
.skeleton-line.shorter {
width: 40%;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`}</style>
</div>
)
}
```
**修正**: `src/components/RecordTabs.jsx`
```javascript
import LoadingSkeleton from './LoadingSkeleton.jsx'
// RecordTabsコンポーネント内
{activeTab === 'lang' && (
loading ? (
<LoadingSkeleton count={3} />
) : (
<RecordList
title={pageContext.isTopPage ? "Latest Lang Records" : "Lang Records for this page"}
records={filteredLangRecords}
apiConfig={apiConfig}
/>
)
)}
```
### 中期実装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で発生している「同じ問題の繰り返し」を避け、
安定した成長可能なシステムが構築できます。

View File

@ -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 }) => (
<div className="loading-skeleton">
{Array(count).fill(0).map((_, i) => (
<div key={i} className="skeleton-item">
<div className="skeleton-avatar"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line short"></div>
</div>
</div>
))}
</div>
)
// 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の問題を回避しながら、
より安定した高性能なシステムに進化できます。

View File

@ -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クライアントが使用されたか確認

View File

@ -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 (
<div className="loading-skeleton">
{showTitle && (
<div className="skeleton-title">
<div className="skeleton-line title"></div>
</div>
)}
{Array(count).fill(0).map((_, i) => (
<div key={i} className="skeleton-item">
<div className="skeleton-avatar"></div>
<div className="skeleton-content">
<div className="skeleton-line name"></div>
<div className="skeleton-line text"></div>
<div className="skeleton-line text short"></div>
<div className="skeleton-line meta"></div>
</div>
</div>
))}
<style jsx>{`
.loading-skeleton {
padding: 10px;
}
.skeleton-title {
margin-bottom: 20px;
}
.skeleton-item {
display: flex;
padding: 15px;
border: 1px solid #eee;
margin: 10px 0;
border-radius: 8px;
background: #fafafa;
}
.skeleton-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
margin-right: 12px;
flex-shrink: 0;
}
.skeleton-content {
flex: 1;
min-width: 0;
}
.skeleton-line {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
margin-bottom: 8px;
border-radius: 4px;
}
.skeleton-line.title {
height: 20px;
width: 30%;
}
.skeleton-line.name {
height: 14px;
width: 25%;
}
.skeleton-line.text {
height: 12px;
width: 90%;
}
.skeleton-line.text.short {
width: 60%;
}
.skeleton-line.meta {
height: 10px;
width: 40%;
margin-bottom: 0;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`}</style>
</div>
)
}
```
### 修正: `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 (
<div className="record-tabs">
<div className="tab-header">
<button
className={`tab-btn ${activeTab === 'lang' ? 'active' : ''}`}
onClick={() => setActiveTab('lang')}
>
Lang Records ({filteredLangRecords?.length || 0})
</button>
<button
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
onClick={() => setActiveTab('comment')}
>
Comment Records ({filteredCommentRecords?.length || 0})
</button>
<button
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
onClick={() => setActiveTab('collection')}
>
Collection ({filteredChatRecords?.length || 0})
</button>
<button
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
onClick={() => setActiveTab('users')}
>
User Comments ({filteredUserComments?.length || 0})
</button>
</div>
<div className="tab-content">
{activeTab === 'lang' && (
!langRecords ? (
<LoadingSkeleton count={3} showTitle={true} />
) : (
<RecordList
title={pageContext.isTopPage ? "Latest Lang Records" : "Lang Records for this page"}
records={filteredLangRecords}
apiConfig={apiConfig}
/>
)
)}
{activeTab === 'comment' && (
!commentRecords ? (
<LoadingSkeleton count={3} showTitle={true} />
) : (
<RecordList
title={pageContext.isTopPage ? "Latest Comment Records" : "Comment Records for this page"}
records={filteredCommentRecords}
apiConfig={apiConfig}
/>
)
)}
{activeTab === 'collection' && (
!chatRecords ? (
<LoadingSkeleton count={2} showTitle={true} />
) : (
<RecordList
title={pageContext.isTopPage ? "Latest Collection Records" : "Collection Records for this page"}
records={filteredChatRecords}
apiConfig={apiConfig}
/>
)
)}
{activeTab === 'users' && (
!userComments ? (
<LoadingSkeleton count={3} showTitle={true} />
) : (
<RecordList
title={pageContext.isTopPage ? "Latest User Comments" : "User Comments for this page"}
records={filteredUserComments}
apiConfig={apiConfig}
/>
)
)}
</div>
{/* 既存のstyle... */}
</div>
)
}
```
### 修正: `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 (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>ATProto OAuth Demo</h1>
<div style={{
background: '#fee',
color: '#c33',
padding: '15px',
borderRadius: '5px',
margin: '20px 0',
border: '1px solid #fcc'
}}>
<p><strong>エラー:</strong> {error}</p>
{retryCount > 0 && (
<p><small>自動リトライ中... ({retryCount}/3)</small></p>
)}
</div>
<button
onClick={refreshAdminData}
style={{
background: '#007bff',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '5px',
cursor: 'pointer'
}}
>
再読み込み
</button>
</div>
)
}
// ... 既存のレンダリング
}
```
## 実装チェックリスト
### ✅ 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: ローディングUI20分
- [ ] `src/components/LoadingSkeleton.jsx` 作成
- [ ] `src/components/RecordTabs.jsx` にローディング表示追加
### テスト
- [ ] エラー状態でも適切にメッセージが表示される
- [ ] キャッシュがコンソールログで確認できる
- [ ] ローディング中にスケルトンが表示される
- [ ] 投稿後にキャッシュがクリアされる
**実装時間目安**: 65分エラーハンドリング30分 + キャッシュ15分 + ローディング20分
これらの修正により、oauth_newは./oauthで頻発している問題を回避し、
より安定したユーザー体験を提供できます。

View File

@ -1,120 +0,0 @@
# OAuth Comment System 開発進捗 - 2025-06-18
## 今日完了した項目
### ✅ UI改善とスタイリング
1. **ヘッダータイトル削除**: "ai.log"タイトルを削除
2. **ログインボタンアイコン化**: テキストからBlueskyアイコン `<i class="fab fa-bluesky"></i>` に変更
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サイトとの統合準備完了

View File

@ -1,222 +0,0 @@
# ATProto OAuth Comment System
ATProtocolBlueskyの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)

View File

@ -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!"