Convex でリアルタイムダッシュボード:KPI/閾値アラート/役割別ビューの実装例
ビジネスアプリケーションにおいて、リアルタイムで KPI を監視し、異常値を即座に検知するダッシュボードは欠かせません。Convex を活用すれば、WebSocket ベースのリアルタイム通信と強力なデータベース機能を組み合わせて、役割別に最適化されたダッシュボードを簡単に構築できます。本記事では、KPI 追跡、閾値アラート、役割別ビューの実装方法を、実践的なコード例とともに解説していきます。
背景
リアルタイムダッシュボードの必要性
現代のビジネス環境では、データドリブンな意思決定が求められています。売上、トラフィック、システムパフォーマンスなどの KPI(重要業績評価指標)をリアルタイムで可視化することで、問題の早期発見や迅速な対応が可能になります。
従来のダッシュボードでは、定期的なポーリングやページリロードが必要でしたが、これではデータの鮮度が保たれず、サーバー負荷も高くなってしまいます。
Convex の特徴とメリット
Convex は、フルスタックアプリケーション向けのバックエンドプラットフォームで、以下の特徴を持っています。
- リアクティブクエリ: データベースの変更が自動的にクライアントに反映される
- TypeScript 完全対応: 型安全な API 開発が可能
- リアルタイム同期: WebSocket による双方向通信を標準搭載
- 認証・認可: 役割ベースのアクセス制御が簡単に実装できる
以下の図は、Convex を使ったリアルタイムダッシュボードの基本アーキテクチャを示しています。
mermaidflowchart TB
user["ユーザー<br/>(各役割)"] -->|アクセス| dashboard["Next.js<br/>ダッシュボード"]
dashboard -->|useQuery| convex["Convex<br/>バックエンド"]
convex -->|WebSocket| dashboard
convex -->|読み書き| db[("Convex DB<br/>KPIデータ")]
subgraph monitoring["監視システム"]
checker["閾値チェッカー"] -->|定期実行| convex
alert["アラート通知"] -->|検知時| convex
end
convex -.->|リアクティブ更新| dashboard
style dashboard fill:#e1f5ff
style convex fill:#fff4e1
style db fill:#f0f0f0
style monitoring fill:#ffe1e1
図の要点:
- ユーザーは Next.js ダッシュボードを通じて KPI データにアクセスします
- Convex が WebSocket でリアルタイムにデータを配信します
- 閾値チェッカーが定期的にデータを監視し、異常を検知します
課題
従来のダッシュボード実装の問題点
リアルタイムダッシュボードを構築する際、以下のような課題に直面します。
| # | 課題 | 具体的な問題 |
|---|---|---|
| 1 | データの鮮度 | ポーリング間隔が長いと最新データが反映されない |
| 2 | サーバー負荷 | 頻繁なポーリングで API サーバーに過負荷がかかる |
| 3 | 実装の複雑さ | WebSocket の接続管理、再接続処理が煩雑 |
| 4 | 型安全性の欠如 | フロントエンドとバックエンドで型の不一致が発生 |
| 5 | 権限管理 | 役割ごとに異なるデータを表示する実装が困難 |
リアルタイム性と権限管理の両立
特に難しいのが、リアルタイム性と細かい権限管理の両立です。例えば、管理者には全 KPI を表示し、営業担当者には売上関連のみ、サポート担当者には顧客満足度のみを表示するといった要件があります。
従来のアプローチでは、クライアント側で権限チェックを行うとセキュリティリスクが高まり、サーバー側で行うと実装が複雑になってしまいます。
以下の図は、役割別のデータアクセス要件を示しています。
mermaidflowchart LR
admin["管理者"] -->|全データ| allkpi["全KPI<br/>売上・顧客・システム"]
sales["営業担当"] -->|売上のみ| saleskpi["売上KPI<br/>受注・商談"]
support["サポート担当"] -->|顧客のみ| supportkpi["顧客KPI<br/>満足度・問い合わせ"]
engineer["エンジニア"] -->|システムのみ| systemkpi["システムKPI<br/>パフォーマンス・エラー"]
style admin fill:#ffcccc
style sales fill:#ccffcc
style support fill:#ccccff
style engineer fill:#ffffcc
解決策
Convex によるリアルタイムダッシュボード設計
Convex を使うことで、上記の課題を以下のように解決できます。
| # | 解決策 | Convex の機能 |
|---|---|---|
| 1 | 自動リアルタイム更新 | リアクティブクエリによる自動再取得 |
| 2 | 効率的な通信 | WebSocket による双方向通信 |
| 3 | シンプルな実装 | useQuery フックで接続管理が不要 |
| 4 | 完全な型安全性 | TypeScript によるエンドツーエンドの型推論 |
| 5 | サーバーサイド認可 | クエリ内での役割チェック |
アーキテクチャ設計
リアルタイムダッシュボードは、以下の 3 つの主要コンポーネントで構成されます。
1. KPI データモデル KPI の種類、値、タイムスタンプ、メタデータを保存します。
2. 閾値アラート機能 定期的に KPI をチェックし、閾値を超えた場合にアラートを生成します。
3. 役割別ビュー ユーザーの役割に応じて、表示する KPI をフィルタリングします。
以下の図は、これらのコンポーネントの連携を示しています。
mermaidsequenceDiagram
participant U as ユーザー
participant D as ダッシュボード
participant C as Convex
participant DB as Database
participant A as アラート
U->>D: ログイン(役割情報付き)
D->>C: useQuery(getKPIs)
C->>DB: 役割に応じたKPI取得
DB-->>C: KPIデータ
C-->>D: リアルタイムデータ配信
D-->>U: 役割別KPI表示
loop 定期チェック
A->>C: 閾値チェック実行
C->>DB: 全KPI取得
DB-->>C: 最新データ
C->>C: 閾値比較
alt 閾値超過
C->>DB: アラート保存
DB-->>D: リアクティブ更新
D-->>U: アラート通知表示
end
end
図で理解できる要点:
- ユーザーの役割情報に基づいて、表示する KPI が動的に決定されます
- 閾値チェックは定期的に自動実行され、異常時のみアラートが生成されます
- データベースの変更は即座にダッシュボードに反映されます
具体例
プロジェクトのセットアップ
まず、Next.js プロジェクトに Convex をセットアップします。
パッケージのインストール
bashyarn create next-app realtime-dashboard --typescript
cd realtime-dashboard
yarn add convex react-chartjs-2 chart.js date-fns
Convex の初期化
bashnpx convex dev
これにより、convexディレクトリが作成され、開発サーバーが起動します。
スキーマ定義
Convex のスキーマは、データベースの構造を型安全に定義します。
KPI テーブルのスキーマ
typescript// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
kpis: defineTable({
// KPIの種類(売上、トラフィック、エラー率など)
type: v.string(),
// KPIの実測値
value: v.number(),
// 単位(円、件、%など)
unit: v.string(),
// 測定日時
timestamp: v.number(),
メタデータとカテゴリ
typescript // カテゴリ(sales, customer, systemなど)
category: v.string(),
// アクセス可能な役割のリスト
allowedRoles: v.array(v.string()),
// 追加のメタデータ
metadata: v.optional(v.object({
source: v.string(),
region: v.optional(v.string()),
department: v.optional(v.string()),
})),
}),
アラートテーブルのスキーマ
typescript alerts: defineTable({
// 関連するKPI ID
kpiId: v.id("kpis"),
// アラートの重要度
severity: v.union(
v.literal("info"),
v.literal("warning"),
v.literal("critical")
),
// アラートメッセージ
message: v.string(),
// 閾値情報
threshold: v.object({
operator: v.string(), // ">", "<", ">=", "<="
value: v.number(),
}),
// 実際の値
actualValue: v.number(),
// 発生日時
createdAt: v.number(),
// 確認済みフラグ
acknowledged: v.boolean(),
}),
閾値設定テーブル
typescript thresholds: defineTable({
// 対象KPIタイプ
kpiType: v.string(),
// 警告レベルの閾値
warningThreshold: v.number(),
// 危機レベルの閾値
criticalThreshold: v.number(),
// 比較演算子(greater_than, less_thanなど)
operator: v.string(),
// 有効/無効フラグ
enabled: v.boolean(),
}),
});
このスキーマ定義により、TypeScript の型推論が効き、フロントエンドからバックエンドまで一貫した型安全性が保証されます。
KPI 取得クエリの実装
次に、役割別に KPI を取得するクエリを実装します。
基本的な KPI 取得クエリ
typescript// convex/kpis.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
// 役割別にKPIを取得するクエリ
export const getKPIs = query({
args: {
// ユーザーの役割
userRole: v.string(),
// 取得期間(オプション)
startTime: v.optional(v.number()),
endTime: v.optional(v.number()),
},
handler: async (ctx, args) => {
const { userRole, startTime, endTime } = args;
権限フィルタリングロジック
typescript// 基本的なクエリ条件
let kpisQuery = ctx.db.query('kpis');
// 期間でフィルタリング
if (startTime && endTime) {
kpisQuery = kpisQuery
.filter((q) => q.gte(q.field('timestamp'), startTime))
.filter((q) => q.lte(q.field('timestamp'), endTime));
}
// 全KPIを取得
const allKpis = await kpisQuery.collect();
役割に基づくアクセス制御
typescript // ユーザーの役割に応じてフィルタリング
const filteredKpis = allKpis.filter((kpi) => {
// 管理者は全データにアクセス可能
if (userRole === "admin") {
return true;
}
// allowedRolesに含まれているかチェック
return kpi.allowedRoles.includes(userRole);
});
return filteredKpis;
},
});
この実装により、サーバーサイドで確実に権限チェックが行われ、クライアント側に不要なデータが送信されることを防ぎます。
KPI 登録ミューテーション
KPI データを登録するためのミューテーションを実装します。
KPI 作成ミューテーション
typescript// convex/kpis.ts に追加
import { mutation } from "./_generated/server";
export const createKPI = mutation({
args: {
type: v.string(),
value: v.number(),
unit: v.string(),
category: v.string(),
allowedRoles: v.array(v.string()),
metadata: v.optional(v.object({
source: v.string(),
region: v.optional(v.string()),
department: v.optional(v.string()),
})),
},
handler: async (ctx, args) => {
データ検証と保存
typescript // 現在時刻をタイムスタンプとして使用
const timestamp = Date.now();
// KPIデータを保存
const kpiId = await ctx.db.insert("kpis", {
type: args.type,
value: args.value,
unit: args.unit,
timestamp,
category: args.category,
allowedRoles: args.allowedRoles,
metadata: args.metadata,
});
return kpiId;
},
});
閾値アラート機能の実装
KPI の値が閾値を超えた場合にアラートを生成する機能を実装します。
閾値チェック用アクション
typescript// convex/alerts.ts
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
// 定期的に実行される閾値チェック
export const checkThresholds = action({
args: {},
handler: async (ctx) => {
// 有効な閾値設定を取得
const thresholds = await ctx.runQuery(
internal.alerts.getActiveThresholds
);
// 各閾値に対してチェック
for (const threshold of thresholds) {
KPI と閾値の比較
typescript// 該当するKPIの最新値を取得
const latestKpi = await ctx.runQuery(
internal.alerts.getLatestKpiByType,
{ kpiType: threshold.kpiType }
);
if (!latestKpi) continue;
// 閾値超過をチェック
const isExceeded = checkThresholdExceeded(
latestKpi.value,
threshold
);
アラート生成ロジック
typescript if (isExceeded) {
// 重要度を判定
const severity = determineSeverity(
latestKpi.value,
threshold
);
// アラートを作成
await ctx.runMutation(internal.alerts.createAlert, {
kpiId: latestKpi._id,
severity,
message: `${latestKpi.type}が閾値を超えました`,
threshold: {
operator: threshold.operator,
value: getSeverityThreshold(threshold, severity),
},
actualValue: latestKpi.value,
});
}
}
},
});
ヘルパー関数の実装
typescript// 閾値超過判定
function checkThresholdExceeded(
value: number,
threshold: any
): boolean {
const { operator, warningThreshold } = threshold;
if (operator === 'greater_than') {
return value > warningThreshold;
} else if (operator === 'less_than') {
return value < warningThreshold;
}
return false;
}
重要度判定関数
typescript// 重要度判定(warning or critical)
function determineSeverity(
value: number,
threshold: any
): 'warning' | 'critical' {
const { operator, criticalThreshold } = threshold;
if (operator === 'greater_than') {
return value > criticalThreshold
? 'critical'
: 'warning';
} else if (operator === 'less_than') {
return value < criticalThreshold
? 'critical'
: 'warning';
}
return 'warning';
}
閾値取得関数
typescript// 重要度に応じた閾値を取得
function getSeverityThreshold(
threshold: any,
severity: 'warning' | 'critical'
): number {
return severity === 'critical'
? threshold.criticalThreshold
: threshold.warningThreshold;
}
内部クエリとミューテーション
アクションから呼び出される内部関数を実装します。
有効な閾値設定の取得
typescript// convex/alerts.ts に追加
import {
internalQuery,
internalMutation,
} from './_generated/server';
export const getActiveThresholds = internalQuery({
args: {},
handler: async (ctx) => {
// 有効な閾値設定のみを取得
return await ctx.db
.query('thresholds')
.filter((q) => q.eq(q.field('enabled'), true))
.collect();
},
});
最新 KPI の取得
typescriptexport const getLatestKpiByType = internalQuery({
args: { kpiType: v.string() },
handler: async (ctx, args) => {
// 指定されたタイプの最新KPIを取得
const kpis = await ctx.db
.query('kpis')
.filter((q) => q.eq(q.field('type'), args.kpiType))
.order('desc') // 降順でソート
.take(1);
return kpis[0] ?? null;
},
});
アラート作成ミューテーション
typescriptexport const createAlert = internalMutation({
args: {
kpiId: v.id('kpis'),
severity: v.union(
v.literal('info'),
v.literal('warning'),
v.literal('critical')
),
message: v.string(),
threshold: v.object({
operator: v.string(),
value: v.number(),
}),
actualValue: v.number(),
},
handler: async (ctx, args) => {
// アラートを作成
const alertId = await ctx.db.insert('alerts', {
kpiId: args.kpiId,
severity: args.severity,
message: args.message,
threshold: args.threshold,
actualValue: args.actualValue,
createdAt: Date.now(),
acknowledged: false,
});
return alertId;
},
});
アラート取得クエリ
ダッシュボードで表示するアラートを取得するクエリを実装します。
未確認アラートの取得
typescript// convex/alerts.ts に追加
export const getUnacknowledgedAlerts = query({
args: {
userRole: v.string(),
},
handler: async (ctx, args) => {
// 未確認のアラートを取得
const alerts = await ctx.db
.query("alerts")
.filter((q) => q.eq(q.field("acknowledged"), false))
.order("desc")
.collect();
アラートの KPI 情報取得
typescript// 各アラートに関連するKPI情報を取得
const alertsWithKpi = await Promise.all(
alerts.map(async (alert) => {
const kpi = await ctx.db.get(alert.kpiId);
// ユーザーの役割でアクセス権チェック
if (!kpi) return null;
if (
args.userRole !== 'admin' &&
!kpi.allowedRoles.includes(args.userRole)
) {
return null;
}
return {
...alert,
kpi,
};
})
);
フィルタリング結果の返却
typescript // nullを除外して返す
return alertsWithKpi.filter((alert) => alert !== null);
},
});
アラート確認ミューテーション
アラートを確認済みにするミューテーションです。
typescript// convex/alerts.ts に追加
export const acknowledgeAlert = mutation({
args: {
alertId: v.id('alerts'),
},
handler: async (ctx, args) => {
// アラートを確認済みに更新
await ctx.db.patch(args.alertId, {
acknowledged: true,
});
return { success: true };
},
});
Next.js でのダッシュボード実装
フロントエンドで Convex のリアルタイム機能を活用したダッシュボードを実装します。
Convex プロバイダーの設定
typescript// app/layout.tsx
'use client';
import {
ConvexProvider,
ConvexReactClient,
} from 'convex/react';
const convex = new ConvexReactClient(
process.env.NEXT_PUBLIC_CONVEX_URL!
);
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='ja'>
<body>
<ConvexProvider client={convex}>
{children}
</ConvexProvider>
</body>
</html>
);
}
ダッシュボードコンポーネント
typescript// app/dashboard/page.tsx
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useState } from "react";
export default function Dashboard() {
// ユーザーの役割(実際は認証システムから取得)
const [userRole, setUserRole] = useState<string>("sales");
リアルタイムデータの取得
typescript// KPIをリアルタイムで取得
const kpis = useQuery(api.kpis.getKPIs, {
userRole,
startTime: Date.now() - 24 * 60 * 60 * 1000, // 過去24時間
endTime: Date.now(),
});
// 未確認アラートを取得
const alerts = useQuery(
api.alerts.getUnacknowledgedAlerts,
{
userRole,
}
);
ミューテーションの準備
typescript// アラート確認用ミューテーション
const acknowledgeAlert = useMutation(
api.alerts.acknowledgeAlert
);
// KPI作成用ミューテーション
const createKPI = useMutation(api.kpis.createKPI);
ローディング状態の処理
typescriptif (kpis === undefined || alerts === undefined) {
return (
<div className='flex items-center justify-center h-screen'>
<div className='text-xl'>データを読み込み中...</div>
</div>
);
}
ダッシュボード UI
typescript return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto">
{/* ヘッダー */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
リアルタイムダッシュボード
</h1>
<p className="text-gray-600 mt-2">
役割: {getRoleName(userRole)}
</p>
</div>
アラート表示セクション
typescript{
/* アラートセクション */
}
{
alerts && alerts.length > 0 && (
<div className='mb-6'>
<h2 className='text-xl font-semibold mb-4'>
未確認アラート ({alerts.length})
</h2>
<div className='space-y-3'>
{alerts.map((alert) => (
<div
key={alert._id}
className={`p-4 rounded-lg border-l-4 ${getSeverityColor(
alert.severity
)}`}
>
<div className='flex justify-between items-start'>
<div>
<p className='font-semibold'>
{alert.message}
</p>
<p className='text-sm text-gray-600 mt-1'>
実測値: {alert.actualValue}{' '}
{alert.kpi?.unit}
(閾値: {alert.threshold.value})
</p>
<p className='text-xs text-gray-500 mt-1'>
{formatTimestamp(alert.createdAt)}
</p>
</div>
<button
onClick={() =>
acknowledgeAlert({
alertId: alert._id,
})
}
className='px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600'
>
確認
</button>
</div>
</div>
))}
</div>
</div>
);
}
KPI 表示セクション
typescript {/* KPIセクション */}
<div>
<h2 className="text-xl font-semibold mb-4">
KPI一覧 ({kpis?.length ?? 0})
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{kpis?.map((kpi) => (
<div
key={kpi._id}
className="bg-white p-6 rounded-lg shadow"
>
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-gray-700">
{kpi.type}
</h3>
<span className="text-xs bg-gray-200 px-2 py-1 rounded">
{kpi.category}
</span>
</div>
<p className="text-3xl font-bold text-blue-600">
{kpi.value.toLocaleString()}
</p>
<p className="text-sm text-gray-600 mt-1">
{kpi.unit}
</p>
<p className="text-xs text-gray-500 mt-2">
{formatTimestamp(kpi.timestamp)}
</p>
</div>
))}
</div>
</div>
</div>
</div>
);
}
ヘルパー関数の実装
UI 表示用のヘルパー関数を定義します。
役割名の日本語化
typescript// app/dashboard/page.tsx に追加
function getRoleName(role: string): string {
const roleNames: Record<string, string> = {
admin: '管理者',
sales: '営業担当',
support: 'サポート担当',
engineer: 'エンジニア',
};
return roleNames[role] ?? role;
}
重要度に応じた色の取得
typescriptfunction getSeverityColor(severity: string): string {
const colors: Record<string, string> = {
info: 'border-blue-500 bg-blue-50',
warning: 'border-yellow-500 bg-yellow-50',
critical: 'border-red-500 bg-red-50',
};
return colors[severity] ?? colors.info;
}
タイムスタンプのフォーマット
typescriptimport { format } from 'date-fns';
import { ja } from 'date-fns/locale';
function formatTimestamp(timestamp: number): string {
return format(
new Date(timestamp),
'yyyy/MM/dd HH:mm:ss',
{
locale: ja,
}
);
}
定期的な閾値チェックの設定
Convex の cron 機能を使って、定期的に閾値チェックを実行します。
cron 設定ファイル
typescript// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';
const crons = cronJobs();
// 1分ごとに閾値チェックを実行
crons.interval(
'check-thresholds',
{ minutes: 1 },
internal.alerts.checkThresholds
);
export default crons;
これにより、1 分ごとに自動的に閾値チェックが実行され、異常があればアラートが生成されてダッシュボードにリアルタイムで反映されます。
テストデータの投入
動作確認のため、テストデータを投入するスクリプトを作成します。
サンプル KPI データ生成
typescript// convex/seedData.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const seedKPIs = mutation({
args: {},
handler: async (ctx) => {
// 売上KPI(営業担当と管理者が閲覧可能)
await ctx.db.insert("kpis", {
type: "月間売上",
value: 15000000,
unit: "円",
timestamp: Date.now(),
category: "sales",
allowedRoles: ["admin", "sales"],
});
複数カテゴリの KPI 生成
typescript// 顧客満足度KPI(サポート担当と管理者が閲覧可能)
await ctx.db.insert('kpis', {
type: '顧客満足度',
value: 4.5,
unit: '点',
timestamp: Date.now(),
category: 'customer',
allowedRoles: ['admin', 'support'],
});
// システムエラー率(エンジニアと管理者が閲覧可能)
await ctx.db.insert('kpis', {
type: 'エラー発生率',
value: 0.02,
unit: '%',
timestamp: Date.now(),
category: 'system',
allowedRoles: ['admin', 'engineer'],
});
閾値設定データの生成
typescript // 閾値設定を追加
await ctx.db.insert("thresholds", {
kpiType: "月間売上",
warningThreshold: 10000000,
criticalThreshold: 5000000,
operator: "less_than",
enabled: true,
});
await ctx.db.insert("thresholds", {
kpiType: "エラー発生率",
warningThreshold: 1.0,
criticalThreshold: 5.0,
operator: "greater_than",
enabled: true,
});
return { success: true };
},
});
グラフ表示の実装
Chart.js を使って KPI の時系列グラフを表示します。
グラフコンポーネント
typescript// components/KPIChart.tsx
'use client';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
// Chart.jsの登録
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
グラフデータの準備
typescriptinterface KPIChartProps {
kpis: Array<{
type: string;
value: number;
timestamp: number;
}>;
kpiType: string;
}
export function KPIChart({ kpis, kpiType }: KPIChartProps) {
// 指定されたタイプのKPIのみをフィルタ
const filteredKpis = kpis
.filter((kpi) => kpi.type === kpiType)
.sort((a, b) => a.timestamp - b.timestamp);
Chart.js 用データ構造
typescriptconst data = {
labels: filteredKpis.map((kpi) =>
new Date(kpi.timestamp).toLocaleTimeString('ja-JP', {
hour: '2-digit',
minute: '2-digit',
})
),
datasets: [
{
label: kpiType,
data: filteredKpis.map((kpi) => kpi.value),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
},
],
};
グラフオプション設定
typescript const options = {
responsive: true,
plugins: {
legend: {
position: "top" as const,
},
title: {
display: true,
text: `${kpiType}の推移`,
},
},
scales: {
y: {
beginAtZero: true,
},
},
};
return <Line data={data} options={options} />;
}
ダッシュボードへのグラフ統合
作成したグラフコンポーネントをダッシュボードに追加します。
グラフセクションの追加
typescript// app/dashboard/page.tsx の return 内に追加
{
/* グラフセクション */
}
<div className='mt-8'>
<h2 className='text-xl font-semibold mb-4'>KPI推移</h2>
<div className='bg-white p-6 rounded-lg shadow'>
{kpis && kpis.length > 0 && (
<KPIChart kpis={kpis} kpiType={kpis[0].type} />
)}
</div>
</div>;
リアルタイム更新の動作確認
実装したダッシュボードのリアルタイム性を確認するための手順です。
開発サーバーの起動
bash# ターミナル1: Convex開発サーバー
npx convex dev
# ターミナル2: Next.js開発サーバー
yarn dev
テストデータの投入確認
ブラウザで http://localhost:3000/dashboard にアクセスし、以下を確認します。
確認ポイント:
- 役割に応じた KPI のみが表示されること
- データが自動的にリアルタイム更新されること
- アラートが表示され、確認ボタンが機能すること
リアルタイム更新のテスト
Convex ダッシュボード(https://dashboard.convex.dev)から直接データを追加し、ブラウザのダッシュボードが即座に更新されることを確認できます。
パフォーマンス最適化
大量の KPI データを扱う場合の最適化手法を紹介します。
ページネーション対応クエリ
typescript// convex/kpis.ts に追加
export const getKPIsPaginated = query({
args: {
userRole: v.string(),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
// 役割に応じたKPIを取得(ページング対応)
const results = await ctx.db
.query("kpis")
.order("desc")
.paginate(args.paginationOpts);
権限フィルタリング付きページネーション
typescript // 権限フィルタリング
const filteredResults = {
...results,
page: results.page.filter((kpi) => {
if (args.userRole === "admin") return true;
return kpi.allowedRoles.includes(args.userRole);
}),
};
return filteredResults;
},
});
インデックスの追加
クエリパフォーマンスを向上させるためのインデックス設定です。
スキーマへのインデックス追加
typescript// convex/schema.ts のkpisテーブルに追加
kpis: defineTable({
// ... 既存のフィールド
})
.index("by_type", ["type"])
.index("by_category", ["category"])
.index("by_timestamp", ["timestamp"]),
これにより、タイプ、カテゴリ、タイムスタンプでの検索が高速化されます。
まとめ
本記事では、Convex を活用したリアルタイムダッシュボードの実装方法について、KPI 追跡、閾値アラート、役割別ビューの 3 つの観点から詳しく解説しました。
Convex の強みは、WebSocket によるリアルタイム通信、TypeScript の型安全性、サーバーサイド認可が簡単に実装できる点にあります。従来のポーリングベースのダッシュボードと比べて、サーバー負荷を抑えながら常に最新のデータを表示できるため、ビジネスの意思決定を迅速化できます。
特に重要なポイントは以下の通りです。
リアクティブクエリによる自動更新 useQuery フックを使うだけで、データベースの変更が自動的に UI に反映されるため、複雑な状態管理が不要になります。
サーバーサイド認可の実装 クエリ内で役割チェックを行うことで、セキュアな権限管理が実現できます。クライアント側に不要なデータが送信されることがありません。
cron 機能による定期処理 閾値チェックなどのバックグラウンド処理を、簡単な設定だけで実現できます。外部のジョブスケジューラーが不要です。
今回実装したダッシュボードは、営業、サポート、エンジニアなど、さまざまな役割のユーザーがそれぞれに最適化された情報をリアルタイムで確認できる仕組みになっています。閾値アラート機能により、問題の早期発見と迅速な対応が可能です。
Convex を使えば、従来は複雑だったリアルタイム機能の実装が驚くほどシンプルになります。ぜひ、自社のダッシュボード構築に活用してみてください。
関連リンク
articleConvex でリアルタイムダッシュボード:KPI/閾値アラート/役割別ビューの実装例
articleConvex で Presence(在席)機能を実装:ユーザーステータスのリアルタイム同期
articleConvex で実践する CQRS/イベントソーシング:履歴・再生・集約の設計ガイド
articleConvex クエリ/ミューテーション/アクション チートシート【保存版シンタックス】
articleConvex 初期設定完全手順:CLI・環境変数・Secrets・権限までゼロから構築
article【比較検証】Convex vs Firebase vs Supabase:リアルタイム性・整合性・学習コストの最適解
articleShell Script とは?初心者が最短で理解する基本構文・実行モデル・活用領域
articleNode.js 本番メモリ運用:ヒープ/外部メモリ/リーク検知の継続監視
articleReact とは? 2025 年版の特徴・強み・実務活用を一気に理解する完全解説
articleNext.js でインフィニットスクロールを実装:Route Handlers +`use` で滑らかデータ読込
articleDocker を用いた統一ローカル環境:新人オンボーディングを 1 日 → 1 時間へ
articleMermaid でデータ基盤のラインジを図解:ETL/DAG/品質チェックの全体像
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来