Skip to main content

🔧 デバッグダッシュボード実装仕様

🎯 実装概要

MVPリリース前の全システム統合テスト用デバッグダッシュボードの詳細実装仕様。

技術スタック

interface TechStack {
frontend: "Next.js 14 + shadcn/ui + Tailwind CSS";
database: "Cloudflare D1";
storage: "Cloudflare R2";
auth: "Cloudflare Access + Firebase Auth";
deployment: "Cloudflare Pages";
monitoring: "Real-time WebSocket + Server-Sent Events";
}

🏗️ アーキテクチャ設計

ファイル構造

/admin-panel/
├── app/
│ ├── debug/
│ │ ├── phase1/ # 基礎インフラテスト
│ │ ├── phase2/ # 認証・セキュリティテスト
│ │ ├── phase3/ # ブランド機能テスト
│ │ ├── phase4/ # 外部連携テスト
│ │ ├── phase5/ # E2Eフローテスト
│ │ └── layout.tsx
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── debug/ # デバッグ専用コンポーネント
│ │ └── test-runner/ # テスト実行エンジン
│ └── lib/
│ ├── test-engine.ts # テスト実行ロジック
│ ├── cloudflare.ts # CF API統合
│ └── monitoring.ts # リアルタイム監視
└── database/
└── schema.sql # D1スキーマ定義

🎨 UI/UXデザイン仕様

デザインシステム

// カラーパレット(デバッグ画面用)
const debugColors = {
success: "hsl(142, 76%, 36%)", // テスト成功
warning: "hsl(38, 92%, 50%)", // 注意・警告
error: "hsl(0, 84%, 60%)", // エラー・失敗
info: "hsl(221, 83%, 53%)", // 情報・進行中
neutral: "hsl(210, 40%, 96%)", // 待機・未実行
brand: {
neko: "hsl(291, 47%, 71%)", // ネコマタ紫
tokinoe: "hsl(142, 69%, 58%)", // 時ノ榮緑
dog: "hsl(25, 95%, 53%)" // ドッグ橙
}
};

レスポンシブレイアウト

// Tailwind CSS クラス構成
const layoutClasses = {
container: "min-h-screen bg-slate-50 dark:bg-slate-900",
sidebar: "w-64 bg-white dark:bg-slate-800 border-r",
main: "flex-1 p-6 space-y-6",
card: "bg-white dark:bg-slate-800 rounded-lg border shadow-sm",
header: "sticky top-0 z-40 bg-white/95 dark:bg-slate-900/95 backdrop-blur"
};

🧪 Phase 1: 基礎インフラテスト

Cloudflare接続テスト

// components/debug/phase1/CloudflareStatus.tsx
interface CloudflareTest {
d1Connection: TestResult;
r2Storage: TestResult;
workersAPI: TestResult;
pagesDeployment: TestResult;
accessAuth: TestResult;
}

const CloudflareStatusCard = () => {
const [tests, setTests] = useState<CloudflareTest>();

const runInfraTests = async () => {
const results = await testRunner.executePhase1([
testD1Connection,
testR2Storage,
testWorkersAPI,
testPagesDeployment,
testAccessAuth
]);
setTests(results);
};

return (
<Card className="p-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Cloud className="h-5 w-5" />
Cloudflare インフラストラクチャ
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(tests || {}).map(([key, result]) => (
<TestStatusBadge key={key} test={key} result={result} />
))}
</div>
<Button onClick={runInfraTests} className="mt-4">
インフラテスト実行
</Button>
</CardContent>
</Card>
);
};

データベース接続テスト

// lib/test-engine/phase1.ts
export const testD1Connection = async (): Promise<TestResult> => {
try {
const start = performance.now();

// D1接続テスト
const db = await getD1Database();
const result = await db.prepare("SELECT 1 as test").first();

const duration = performance.now() - start;

return {
status: result ? "success" : "error",
message: result ? "D1接続成功" : "D1接続失敗",
duration: Math.round(duration),
details: { result, connectionTime: duration }
};
} catch (error) {
return {
status: "error",
message: `D1エラー: ${error.message}`,
duration: 0,
details: { error }
};
}
};

🔐 Phase 2: 認証・セキュリティテスト

App Check統合テスト

// components/debug/phase2/AppCheckTest.tsx
interface AppCheckResults {
web: {
recaptcha: TestResult;
tokenGeneration: TestResult;
tokenVerification: TestResult;
};
ios: {
deviceCheck: TestResult;
attestation: TestResult;
};
android: {
playIntegrity: TestResult;
safetyNet: TestResult;
};
}

const AppCheckTestSuite = () => {
const [results, setResults] = useState<AppCheckResults>();

const runAppCheckTests = async () => {
// Web reCAPTCHA テスト
const webTests = await Promise.all([
testRecaptchaLoad(),
testTokenGeneration(),
testTokenVerification()
]);

// モバイルApp Check テスト(シミュレーション)
const mobileTests = await Promise.all([
simulateDeviceCheck(),
simulatePlayIntegrity()
]);

setResults({
web: {
recaptcha: webTests[0],
tokenGeneration: webTests[1],
tokenVerification: webTests[2]
},
ios: { deviceCheck: mobileTests[0], attestation: mobileTests[0] },
android: { playIntegrity: mobileTests[1], safetyNet: mobileTests[1] }
});
};

return (
<Card className="p-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Firebase App Check 統合テスト
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="web">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="web">Web (reCAPTCHA)</TabsTrigger>
<TabsTrigger value="ios">iOS (DeviceCheck)</TabsTrigger>
<TabsTrigger value="android">Android (Play Integrity)</TabsTrigger>
</TabsList>

<TabsContent value="web" className="space-y-4">
<div className="grid gap-4">
<TestResultCard
title="reCAPTCHA読み込み"
result={results?.web.recaptcha}
/>
<TestResultCard
title="トークン生成"
result={results?.web.tokenGeneration}
/>
<TestResultCard
title="トークン検証"
result={results?.web.tokenVerification}
/>
</div>
</TabsContent>

<TabsContent value="ios" className="space-y-4">
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertTitle>iOS テスト</AlertTitle>
<AlertDescription>
実機テストが必要です。シミュレーション結果を表示中。
</AlertDescription>
</Alert>
<TestResultCard
title="DeviceCheck"
result={results?.ios.deviceCheck}
/>
</TabsContent>

<TabsContent value="android" className="space-y-4">
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Android テスト</AlertTitle>
<AlertDescription>
実機テストが必要です。シミュレーション結果を表示中。
</AlertDescription>
</Alert>
<TestResultCard
title="Play Integrity API"
result={results?.android.playIntegrity}
/>
</TabsContent>
</Tabs>

<Button onClick={runAppCheckTests} className="mt-4 w-full">
App Check テスト実行
</Button>
</CardContent>
</Card>
);
};

📷 Phase 3: 写真アップロード・サムネイルテスト

画像処理テストコンポーネント

// components/debug/phase3/ImageUploadTest.tsx
interface ImageTestResult {
originalSize: number;
compressedSize: number;
thumbnailSize: number;
processingTime: number;
uploadTime: number;
thumbnailDisplayTime: number;
success: boolean;
}

const ImageUploadTestSuite = () => {
const [testResults, setTestResults] = useState<ImageTestResult[]>([]);
const [currentTest, setCurrentTest] = useState<string>("");
const [thumbnailUrl, setThumbnailUrl] = useState<string>("");

const runImageTest = async (file: File) => {
setCurrentTest("画像処理中...");

const startTime = performance.now();

try {
// 1. 画像圧縮(印刷用)
const compressedPrint = await compressImage(file, {
width: 1051,
height: 1051,
quality: 0.9,
format: "JPEG"
});

// 2. サムネイル生成
const thumbnail = await compressImage(file, {
width: 300,
height: 300,
quality: 0.8,
format: "JPEG"
});

const processingTime = performance.now() - startTime;
setCurrentTest("アップロード中...");

// 3. R2アップロード
const uploadStart = performance.now();
const uploadResults = await Promise.all([
uploadToR2(compressedPrint, "prints/"),
uploadToR2(thumbnail, "thumbnails/")
]);
const uploadTime = performance.now() - uploadStart;

// 4. サムネイル表示テスト
setCurrentTest("サムネイル表示テスト中...");
const thumbnailDisplayStart = performance.now();

const thumbnailPublicUrl = `https://r2.contents-print.jp/thumbnails/${uploadResults[1].key}`;
setThumbnailUrl(thumbnailPublicUrl);

// サムネイル読み込み完了を待機
await waitForImageLoad(thumbnailPublicUrl);
const thumbnailDisplayTime = performance.now() - thumbnailDisplayStart;

// 5. テスト結果記録
const result: ImageTestResult = {
originalSize: file.size,
compressedSize: compressedPrint.size,
thumbnailSize: thumbnail.size,
processingTime: Math.round(processingTime),
uploadTime: Math.round(uploadTime),
thumbnailDisplayTime: Math.round(thumbnailDisplayTime),
success: true
};

setTestResults(prev => [...prev, result]);
setCurrentTest("テスト完了");

} catch (error) {
console.error("画像テストエラー:", error);
setCurrentTest(`エラー: ${error.message}`);

setTestResults(prev => [...prev, {
originalSize: file.size,
compressedSize: 0,
thumbnailSize: 0,
processingTime: 0,
uploadTime: 0,
thumbnailDisplayTime: 0,
success: false
}]);
}
};

return (
<Card className="p-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Camera className="h-5 w-5" />
写真アップロード・サムネイルテスト
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* ファイル選択・テスト実行 */}
<div className="space-y-4">
<Label htmlFor="test-image">テスト画像を選択</Label>
<Input
id="test-image"
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) runImageTest(file);
}}
/>
{currentTest && (
<Alert>
<Loader2 className="h-4 w-4 animate-spin" />
<AlertDescription>{currentTest}</AlertDescription>
</Alert>
)}
</div>

{/* サムネイル表示テスト */}
{thumbnailUrl && (
<div className="space-y-2">
<Label>サムネイル表示確認</Label>
<div className="border rounded-lg p-4 bg-gray-50">
<img
src={thumbnailUrl}
alt="テストサムネイル"
className="w-32 h-32 object-cover rounded"
onLoad={() => console.log("サムネイル表示成功")}
onError={() => console.error("サムネイル表示失敗")}
/>
</div>
</div>
)}

{/* テスト結果表示 */}
{testResults.length > 0 && (
<div className="space-y-4">
<Label>テスト結果履歴</Label>
<div className="space-y-2">
{testResults.map((result, index) => (
<Card key={index} className={`p-4 ${result.success ? 'border-green-200' : 'border-red-200'}`}>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium">処理時間:</span>
<span className="ml-2">{result.processingTime}ms</span>
</div>
<div>
<span className="font-medium">アップロード:</span>
<span className="ml-2">{result.uploadTime}ms</span>
</div>
<div>
<span className="font-medium">サムネイル表示:</span>
<span className="ml-2">{result.thumbnailDisplayTime}ms</span>
</div>
<div>
<span className="font-medium">圧縮率:</span>
<span className="ml-2">
{((1 - result.compressedSize / result.originalSize) * 100).toFixed(1)}%
</span>
</div>
<div>
<span className="font-medium">サムネイルサイズ:</span>
<span className="ml-2">{(result.thumbnailSize / 1024).toFixed(1)}KB</span>
</div>
<div>
<Badge variant={result.success ? "default" : "destructive"}>
{result.success ? "成功" : "失敗"}
</Badge>
</div>
</div>
</Card>
))}
</div>
</div>
)}

{/* パフォーマンス基準 */}
<Alert>
<Target className="h-4 w-4" />
<AlertTitle>パフォーマンス目標</AlertTitle>
<AlertDescription>
<ul className="mt-2 space-y-1 text-sm">
<li>• 画像処理: &lt;5</li>
<li>• アップロード: &lt;10</li>
<li>• サムネイル表示: &lt;2</li>
<li>• 圧縮率: 60%以上</li>
</ul>
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
};

🔄 リアルタイム監視システム

WebSocket監視ダッシュボード

// components/debug/MonitoringDashboard.tsx
interface SystemMetrics {
cloudflare: {
d1: { responseTime: number; status: "healthy" | "warning" | "error" };
r2: { responseTime: number; status: "healthy" | "warning" | "error" };
workers: { responseTime: number; status: "healthy" | "warning" | "error" };
};
firebase: {
auth: { responseTime: number; status: "healthy" | "warning" | "error" };
appCheck: { responseTime: number; status: "healthy" | "warning" | "error" };
};
shopify: {
api: { responseTime: number; status: "healthy" | "warning" | "error" };
webhooks: { lastReceived: Date; status: "healthy" | "warning" | "error" };
};
}

const MonitoringDashboard = () => {
const [metrics, setMetrics] = useState<SystemMetrics>();
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
// Server-Sent Events接続
const eventSource = new EventSource('/api/debug/monitoring');

eventSource.onopen = () => setIsConnected(true);
eventSource.onclose = () => setIsConnected(false);

eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setMetrics(data);
};

return () => eventSource.close();
}, []);

return (
<Card className="p-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
リアルタイム システム監視
<Badge variant={isConnected ? "default" : "destructive"} className="ml-auto">
{isConnected ? "接続中" : "切断"}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Cloudflare監視 */}
<div className="space-y-4">
<h3 className="font-semibold flex items-center gap-2">
<Cloud className="h-4 w-4" />
Cloudflare
</h3>
{metrics?.cloudflare && Object.entries(metrics.cloudflare).map(([service, data]) => (
<div key={service} className="flex items-center justify-between p-3 border rounded">
<span className="capitalize">{service}</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{data.responseTime}ms</span>
<StatusIndicator status={data.status} />
</div>
</div>
))}
</div>

{/* Firebase監視 */}
<div className="space-y-4">
<h3 className="font-semibold flex items-center gap-2">
<Shield className="h-4 w-4" />
Firebase
</h3>
{metrics?.firebase && Object.entries(metrics.firebase).map(([service, data]) => (
<div key={service} className="flex items-center justify-between p-3 border rounded">
<span className="capitalize">{service}</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{data.responseTime}ms</span>
<StatusIndicator status={data.status} />
</div>
</div>
))}
</div>

{/* Shopify監視 */}
<div className="space-y-4">
<h3 className="font-semibold flex items-center gap-2">
<ShoppingCart className="h-4 w-4" />
Shopify
</h3>
{metrics?.shopify && Object.entries(metrics.shopify).map(([service, data]) => (
<div key={service} className="flex items-center justify-between p-3 border rounded">
<span className="capitalize">{service}</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">
{service === 'webhooks'
? formatDistanceToNow(data.lastReceived)
: `${data.responseTime}ms`
}
</span>
<StatusIndicator status={data.status} />
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};

const StatusIndicator = ({ status }: { status: "healthy" | "warning" | "error" }) => {
const config = {
healthy: { color: "bg-green-500", text: "正常" },
warning: { color: "bg-yellow-500", text: "警告" },
error: { color: "bg-red-500", text: "エラー" }
};

return (
<div className="flex items-center gap-1">
<div className={`w-2 h-2 rounded-full ${config[status].color}`} />
<span className="text-xs">{config[status].text}</span>
</div>
);
};

🚀 デプロイ・実行設定

Cloudflare Pages設定

# wrangler.toml
name = "contents-print-debug"
compatibility_date = "2024-08-23"

[env.production]
route = "debug.contents-print.jp"

[[env.production.d1_databases]]
binding = "DB"
database_name = "contents-print-debug"
database_id = "your-d1-database-id"

[[env.production.r2_buckets]]
binding = "STORAGE"
bucket_name = "contents-print-storage"

環境変数設定

# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id

CLOUDFLARE_API_TOKEN=your_cloudflare_token
CLOUDFLARE_ACCOUNT_ID=your_account_id

SHOPIFY_STORE_URL=your_store_url
SHOPIFY_ACCESS_TOKEN=your_access_token

📊 テスト実行・レポート機能

自動テストスケジューラー

// lib/test-scheduler.ts
export const scheduleAutomaticTests = () => {
// 5分間隔で基礎インフラテスト
setInterval(async () => {
const results = await testRunner.executePhase1();
await saveTestResults(results, 'phase1');
}, 5 * 60 * 1000);

// 15分間隔で認証テスト
setInterval(async () => {
const results = await testRunner.executePhase2();
await saveTestResults(results, 'phase2');
}, 15 * 60 * 1000);

// 1時間間隔で外部連携テスト
setInterval(async () => {
const results = await testRunner.executePhase4();
await saveTestResults(results, 'phase4');
}, 60 * 60 * 1000);
};

テスト結果エクスポート

// components/debug/TestReportExport.tsx
const TestReportExport = () => {
const exportReport = async (format: 'json' | 'csv') => {
const testData = await fetchAllTestResults();

if (format === 'json') {
downloadJSON(testData, `debug-report-${new Date().toISOString()}.json`);
} else {
downloadCSV(formatAsCSV(testData), `debug-report-${new Date().toISOString()}.csv`);
}
};

return (
<div className="flex gap-2">
<Button variant="outline" onClick={() => exportReport('json')}>
<Download className="h-4 w-4 mr-2" />
JSON エクスポート
</Button>
<Button variant="outline" onClick={() => exportReport('csv')}>
<Download className="h-4 w-4 mr-2" />
CSV エクスポート
</Button>
</div>
);
};

実装優先度: Phase 1 → Phase 2 → Phase 3 → リアルタイム監視 → Phase 4・5

関連ドキュメント: Web管理画面詳細 | システム設計