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:
copilot-swe-agent[bot]
2026-03-26 18:12:22 +00:00
parent 276ef1f8b2
commit fd45c838fc
13 changed files with 977 additions and 20 deletions
+13 -1
View File
@@ -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>
)}
+11 -7
View File
@@ -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">
+5 -1
View File
@@ -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
View File
@@ -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;