feat: Add logging and reporting capabilities with GDPR compliance
Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/d6e65ca9-f07f-43a2-bac2-09eb44588ded Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -89,7 +89,7 @@ export default function AdminPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
<Link
|
||||
href="/admin/users"
|
||||
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
|
||||
@@ -114,6 +114,18 @@ export default function AdminPage() {
|
||||
<p className="text-sm text-gray-500">Create and configure subscription plans</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/logs"
|
||||
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
|
||||
>
|
||||
<div className="p-3 rounded-full bg-green-100 group-hover:bg-green-200 transition-colors">
|
||||
<Activity className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-gray-900">Activity Logs</p>
|
||||
<p className="text-sm text-gray-500">Processing runs and per-email logs across all users</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthGuard } from '@/components/AuthGuard';
|
||||
import { DashboardLayout } from '@/components/DashboardLayout';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { mailAccountsApi, processingRunsApi } from '@/lib/api';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Mail,
|
||||
Send,
|
||||
@@ -51,19 +52,19 @@ export default function DashboardPage() {
|
||||
|
||||
const { data: runs, isLoading: runsLoading } = useQuery({
|
||||
queryKey: ['processing-runs'],
|
||||
queryFn: () => processingRunsApi.list(),
|
||||
queryFn: () => processingRunsApi.list({ page: 1, page_size: 10 }),
|
||||
});
|
||||
|
||||
const stats = {
|
||||
totalAccounts: accounts?.length || 0,
|
||||
activeAccounts: accounts?.filter((a) => a.is_enabled).length || 0,
|
||||
emailsToday: runs
|
||||
emailsToday: runs?.items
|
||||
?.filter((r) => {
|
||||
const today = new Date().toDateString();
|
||||
return new Date(r.started_at).toDateString() === today;
|
||||
})
|
||||
.reduce((sum, r) => sum + r.emails_forwarded, 0) || 0,
|
||||
errors: runs?.filter((r) => r.emails_failed > 0).length || 0,
|
||||
errors: runs?.items?.filter((r) => r.emails_failed > 0).length || 0,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -100,15 +101,18 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Recent Processing Runs */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Recent Processing Runs</h3>
|
||||
<Link href="/logs" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||
View all logs →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
{runsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : runs && runs.length > 0 ? (
|
||||
) : runs && runs.items && runs.items.length > 0 ? (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
@@ -133,12 +137,12 @@ export default function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{runs.slice(0, 10).map((run) => {
|
||||
{runs.items.map((run) => {
|
||||
const account = accounts?.find((a) => a.id === run.mail_account_id);
|
||||
return (
|
||||
<tr key={run.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{account?.name || `Account ${run.mail_account_id}`}
|
||||
{run.account_name || account?.name || `Account ${run.mail_account_id}`}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
User,
|
||||
Shield,
|
||||
Users,
|
||||
CreditCard
|
||||
CreditCard,
|
||||
FileText,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
@@ -35,6 +37,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Mail Accounts', href: '/accounts', icon: Mail },
|
||||
{ name: 'Logs', href: '/logs', icon: FileText },
|
||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
@@ -43,6 +46,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{ name: 'Admin Overview', href: '/admin', icon: Shield },
|
||||
{ name: 'Manage Users', href: '/admin/users', icon: Users },
|
||||
{ name: 'Manage Plans', href: '/admin/plans', icon: CreditCard },
|
||||
{ name: 'Activity Logs', href: '/admin/logs', icon: Activity },
|
||||
]
|
||||
: [];
|
||||
|
||||
|
||||
+133
-4
@@ -126,6 +126,64 @@ export interface ProcessingRun {
|
||||
emails_failed: number;
|
||||
status: string;
|
||||
error_message?: string | null;
|
||||
account_name?: string | null;
|
||||
account_email?: string | null;
|
||||
}
|
||||
|
||||
export interface ProcessingLog {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
email_subject?: string | null;
|
||||
email_from?: string | null;
|
||||
success: boolean;
|
||||
mail_account_id: number;
|
||||
processing_run_id?: number | null;
|
||||
email_size_bytes?: number | null;
|
||||
error_details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface PaginatedProcessingRuns {
|
||||
items: ProcessingRun[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface PaginatedProcessingLogs {
|
||||
items: ProcessingLog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface AdminProcessingRun extends ProcessingRun {
|
||||
user_id?: number | null;
|
||||
user_email?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProcessingLog extends ProcessingLog {
|
||||
user_id: number;
|
||||
user_email?: string | null;
|
||||
}
|
||||
|
||||
export interface PaginatedAdminRuns {
|
||||
items: AdminProcessingRun[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface PaginatedAdminLogsResponse {
|
||||
items: AdminProcessingLog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface AutoDetectSuggestion {
|
||||
@@ -292,10 +350,54 @@ export const mailAccountsApi = {
|
||||
// ── Processing Runs API ─────────────────────────────────────────────────
|
||||
|
||||
export const processingRunsApi = {
|
||||
async list(): Promise<ProcessingRun[]> {
|
||||
// TODO: Add a dedicated /processing-runs endpoint to the backend
|
||||
// For now, return empty array since no user-facing endpoint exists yet
|
||||
return [];
|
||||
async list(params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
account_id?: number;
|
||||
status?: string;
|
||||
}): Promise<PaginatedProcessingRuns> {
|
||||
const response = await api.get<PaginatedProcessingRuns>("/processing-runs", {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async get(runId: number): Promise<ProcessingRun> {
|
||||
const response = await api.get<ProcessingRun>(`/processing-runs/${runId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getLogs(
|
||||
runId: number,
|
||||
params?: { page?: number; page_size?: number }
|
||||
): Promise<PaginatedProcessingLogs> {
|
||||
const response = await api.get<PaginatedProcessingLogs>(
|
||||
`/processing-runs/${runId}/logs`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async listForAccount(
|
||||
accountId: number,
|
||||
params?: { page?: number; page_size?: number; status?: string }
|
||||
): Promise<PaginatedProcessingRuns> {
|
||||
const response = await api.get<PaginatedProcessingRuns>(
|
||||
`/mail-accounts/${accountId}/processing-runs`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async listLogsForAccount(
|
||||
accountId: number,
|
||||
params?: { page?: number; page_size?: number; level?: string }
|
||||
): Promise<PaginatedProcessingLogs> {
|
||||
const response = await api.get<PaginatedProcessingLogs>(
|
||||
`/mail-accounts/${accountId}/logs`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -483,6 +585,33 @@ export const adminApi = {
|
||||
async deletePlan(id: number): Promise<void> {
|
||||
await api.delete(`/admin/plans/${id}`);
|
||||
},
|
||||
|
||||
async listProcessingRuns(params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
user_id?: number;
|
||||
account_id?: number;
|
||||
status?: string;
|
||||
}): Promise<PaginatedAdminRuns> {
|
||||
const response = await api.get<PaginatedAdminRuns>('/admin/processing-runs', {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async listProcessingLogs(params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
user_id?: number;
|
||||
account_id?: number;
|
||||
run_id?: number;
|
||||
level?: string;
|
||||
}): Promise<PaginatedAdminLogsResponse> {
|
||||
const response = await api.get<PaginatedAdminLogsResponse>('/admin/processing-logs', {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
Reference in New Issue
Block a user