- Add Legal Office of the Master seal (SVG design with Maltese Cross, scales of justice, legal scroll) - Create legal-office-manifest-template.json for Legal Office credentials - Update SEAL_MAPPING.md and DESIGN_GUIDE.md with Legal Office seal documentation - Complete Azure CDN infrastructure deployment: - Resource group, storage account, and container created - 17 PNG seal files uploaded to Azure Blob Storage - All manifest templates updated with Azure URLs - Configuration files generated (azure-cdn-config.env) - Add comprehensive Azure CDN setup scripts and documentation - Fix manifest URL generation to prevent double slashes - Verify all seals accessible via HTTPS
251 lines
7.0 KiB
TypeScript
251 lines
7.0 KiB
TypeScript
/**
|
|
* Credential issuance metrics and dashboard
|
|
* Real-time metrics: issued per day/week/month, success/failure rates, average issuance time, credential types distribution
|
|
*/
|
|
|
|
import { getAuditStatistics, searchAuditLogs } from '@the-order/database';
|
|
// import { getPool } from '@the-order/database'; // Not used in this file
|
|
import { query } from '@the-order/database';
|
|
|
|
export interface CredentialMetrics {
|
|
// Time-based metrics
|
|
issuedToday: number;
|
|
issuedThisWeek: number;
|
|
issuedThisMonth: number;
|
|
issuedThisYear: number;
|
|
|
|
// Success/failure rates
|
|
successRate: number; // percentage
|
|
failureRate: number; // percentage
|
|
totalIssuances: number;
|
|
totalFailures: number;
|
|
|
|
// Performance metrics
|
|
averageIssuanceTime: number; // milliseconds
|
|
p50IssuanceTime: number; // milliseconds
|
|
p95IssuanceTime: number; // milliseconds
|
|
p99IssuanceTime: number; // milliseconds
|
|
|
|
// Credential type distribution
|
|
byCredentialType: Record<string, number>;
|
|
byAction: Record<string, number>;
|
|
|
|
// Recent activity
|
|
recentIssuances: Array<{
|
|
credentialId: string;
|
|
credentialType: string[];
|
|
issuedAt: Date;
|
|
subjectDid: string;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Get credential issuance metrics
|
|
*/
|
|
export async function getCredentialMetrics(
|
|
startDate?: Date,
|
|
endDate?: Date
|
|
): Promise<CredentialMetrics> {
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
const yearAgo = new Date(today.getFullYear(), 0, 1);
|
|
|
|
// Get statistics
|
|
const stats = await getAuditStatistics(startDate || yearAgo, endDate || now);
|
|
|
|
// Get time-based counts
|
|
const issuedToday = await getIssuanceCount(today, now);
|
|
const issuedThisWeek = await getIssuanceCount(weekAgo, now);
|
|
const issuedThisMonth = await getIssuanceCount(monthAgo, now);
|
|
const issuedThisYear = await getIssuanceCount(yearAgo, now);
|
|
|
|
// Get performance metrics (would need to track issuance time in audit log)
|
|
const performanceMetrics = await getPerformanceMetrics(startDate || yearAgo, endDate || now);
|
|
|
|
// Get recent issuances
|
|
const recentIssuancesResult = await searchAuditLogs(
|
|
{ action: 'issued' },
|
|
1,
|
|
10
|
|
);
|
|
|
|
const recentIssuances = recentIssuancesResult.logs.map((log) => ({
|
|
credentialId: log.credential_id,
|
|
credentialType: log.credential_type,
|
|
issuedAt: log.performed_at,
|
|
subjectDid: log.subject_did,
|
|
}));
|
|
|
|
// Calculate success/failure rates
|
|
const totalIssuances = stats.totalIssuances;
|
|
const totalFailures = 0; // Would need to track failures separately
|
|
const successRate = totalIssuances > 0 ? ((totalIssuances - totalFailures) / totalIssuances) * 100 : 100;
|
|
const failureRate = totalIssuances > 0 ? (totalFailures / totalIssuances) * 100 : 0;
|
|
|
|
return {
|
|
issuedToday,
|
|
issuedThisWeek,
|
|
issuedThisMonth,
|
|
issuedThisYear,
|
|
successRate,
|
|
failureRate,
|
|
totalIssuances,
|
|
totalFailures,
|
|
averageIssuanceTime: performanceMetrics.average,
|
|
p50IssuanceTime: performanceMetrics.p50,
|
|
p95IssuanceTime: performanceMetrics.p95,
|
|
p99IssuanceTime: performanceMetrics.p99,
|
|
byCredentialType: stats.byCredentialType,
|
|
byAction: stats.byAction,
|
|
recentIssuances,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get issuance count for time period
|
|
*/
|
|
async function getIssuanceCount(startDate: Date, endDate: Date): Promise<number> {
|
|
const result = await query<{ count: string }>(
|
|
`SELECT COUNT(*) as count
|
|
FROM credential_issuance_audit
|
|
WHERE action = 'issued'
|
|
AND performed_at >= $1
|
|
AND performed_at <= $2`,
|
|
[startDate, endDate]
|
|
);
|
|
return parseInt(result.rows[0]?.count || '0', 10);
|
|
}
|
|
|
|
/**
|
|
* Get performance metrics
|
|
* Note: This requires tracking issuance time in the audit log metadata
|
|
*/
|
|
async function getPerformanceMetrics(
|
|
_startDate: Date,
|
|
_endDate: Date
|
|
): Promise<{
|
|
average: number;
|
|
p50: number;
|
|
p95: number;
|
|
p99: number;
|
|
}> {
|
|
// In production, this would query metadata for issuance times
|
|
// For now, return placeholder values
|
|
return {
|
|
average: 500, // milliseconds
|
|
p50: 400,
|
|
p95: 1000,
|
|
p99: 2000,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get metrics dashboard data
|
|
*/
|
|
export async function getMetricsDashboard(): Promise<{
|
|
summary: CredentialMetrics;
|
|
trends: {
|
|
daily: Array<{ date: string; count: number }>;
|
|
weekly: Array<{ week: string; count: number }>;
|
|
monthly: Array<{ month: string; count: number }>;
|
|
};
|
|
topCredentialTypes: Array<{ type: string; count: number; percentage: number }>;
|
|
}> {
|
|
const summary = await getCredentialMetrics();
|
|
|
|
// Get daily trends (last 30 days)
|
|
const dailyTrends = await getDailyTrends(30);
|
|
const weeklyTrends = await getWeeklyTrends(12);
|
|
const monthlyTrends = await getMonthlyTrends(12);
|
|
|
|
// Calculate top credential types
|
|
const total = Object.values(summary.byCredentialType).reduce((sum, count) => sum + count, 0);
|
|
const topCredentialTypes = Object.entries(summary.byCredentialType)
|
|
.map(([type, count]) => ({
|
|
type,
|
|
count,
|
|
percentage: total > 0 ? (count / total) * 100 : 0,
|
|
}))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 10);
|
|
|
|
return {
|
|
summary,
|
|
trends: {
|
|
daily: dailyTrends,
|
|
weekly: weeklyTrends,
|
|
monthly: monthlyTrends,
|
|
},
|
|
topCredentialTypes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get daily trends
|
|
*/
|
|
async function getDailyTrends(days: number): Promise<Array<{ date: string; count: number }>> {
|
|
const result = await query<{ date: string; count: string }>(
|
|
`SELECT
|
|
DATE(performed_at) as date,
|
|
COUNT(*) as count
|
|
FROM credential_issuance_audit
|
|
WHERE action = 'issued'
|
|
AND performed_at >= NOW() - INTERVAL '1 day' * $1
|
|
GROUP BY DATE(performed_at)
|
|
ORDER BY date DESC`,
|
|
[days]
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
date: row.date,
|
|
count: parseInt(row.count, 10),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get weekly trends
|
|
*/
|
|
async function getWeeklyTrends(weeks: number): Promise<Array<{ week: string; count: number }>> {
|
|
const result = await query<{ week: string; count: string }>(
|
|
`SELECT
|
|
DATE_TRUNC('week', performed_at) as week,
|
|
COUNT(*) as count
|
|
FROM credential_issuance_audit
|
|
WHERE action = 'issued'
|
|
AND performed_at >= NOW() - INTERVAL '1 week' * $1
|
|
GROUP BY DATE_TRUNC('week', performed_at)
|
|
ORDER BY week DESC`,
|
|
[weeks]
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
week: row.week,
|
|
count: parseInt(row.count, 10),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get monthly trends
|
|
*/
|
|
async function getMonthlyTrends(months: number): Promise<Array<{ month: string; count: number }>> {
|
|
const result = await query<{ month: string; count: string }>(
|
|
`SELECT
|
|
DATE_TRUNC('month', performed_at) as month,
|
|
COUNT(*) as count
|
|
FROM credential_issuance_audit
|
|
WHERE action = 'issued'
|
|
AND performed_at >= NOW() - INTERVAL '1 month' * $1
|
|
GROUP BY DATE_TRUNC('month', performed_at)
|
|
ORDER BY month DESC`,
|
|
[months]
|
|
);
|
|
|
|
return result.rows.map((row) => ({
|
|
month: row.month,
|
|
count: parseInt(row.count, 10),
|
|
}));
|
|
}
|
|
|