From 82829661da91721d2aff0f2ac0d1f0787ec12069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:19:52 +0000 Subject: [PATCH] fix: track alembic migrations and logs pages (fix .gitignore exclusions) Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/fccf042a-a3d9-4eff-a897-9bb3650629cc Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .gitignore | 2 - .../versions/0001_add_notification_columns.py | 43 +++ frontend/src/app/admin/logs/page.tsx | 223 +++++++++++++ frontend/src/app/logs/page.tsx | 293 ++++++++++++++++++ 4 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 backend/alembic/versions/0001_add_notification_columns.py create mode 100644 frontend/src/app/admin/logs/page.tsx create mode 100644 frontend/src/app/logs/page.tsx diff --git a/.gitignore b/.gitignore index 7a81398..7b07881 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,6 @@ env/ # Logs *.log -logs/ # OS .DS_Store @@ -55,7 +54,6 @@ htmlcov/ # Backend specific backend/.env -backend/alembic/versions/*_*.py backend/*.db backend/*.sqlite diff --git a/backend/alembic/versions/0001_add_notification_columns.py b/backend/alembic/versions/0001_add_notification_columns.py new file mode 100644 index 0000000..ada474d --- /dev/null +++ b/backend/alembic/versions/0001_add_notification_columns.py @@ -0,0 +1,43 @@ +"""Add name and apprise_url columns to notification_configs + +Revision ID: 0001 +Revises: +Create Date: 2026-03-26 + +These two columns were added to the NotificationConfig ORM model in the +Apprise alerting feature PR. SQLAlchemy's create_all() does not ALTER +existing tables, so deployments that had notification_configs created before +this change are missing the columns and raise a ProgrammingError at runtime. + +Using ADD COLUMN IF NOT EXISTS makes this migration idempotent – it is safe +to run against both fresh installs (where create_all already created the +columns) and existing deployments (where the columns are absent). +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0001" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + "ALTER TABLE notification_configs " + "ADD COLUMN IF NOT EXISTS name VARCHAR(255) NOT NULL DEFAULT 'My Notification'" + ) + op.execute( + "ALTER TABLE notification_configs " + "ADD COLUMN IF NOT EXISTS apprise_url TEXT" + ) + + +def downgrade() -> None: + op.execute( + "ALTER TABLE notification_configs DROP COLUMN IF EXISTS apprise_url" + ) + op.execute( + "ALTER TABLE notification_configs DROP COLUMN IF EXISTS name" + ) diff --git a/frontend/src/app/admin/logs/page.tsx b/frontend/src/app/admin/logs/page.tsx new file mode 100644 index 0000000..c2c8c23 --- /dev/null +++ b/frontend/src/app/admin/logs/page.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useState } from 'react'; +import { AuthGuard } from '@/components/AuthGuard'; +import { DashboardLayout } from '@/components/DashboardLayout'; +import { useQuery } from '@tanstack/react-query'; +import { useAuthStore } from '@/store/authStore'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { adminApi, AdminProcessingRun } from '@/lib/api'; +import { + Activity, + ChevronLeft, + ChevronRight, + CheckCircle, + XCircle, + RefreshCw, +} from 'lucide-react'; + +const STATUS_STYLES: Record = { + completed: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + partial_failure: 'bg-yellow-100 text-yellow-800', + running: 'bg-blue-100 text-blue-800', +}; + +function formatDuration(seconds?: number | null): string { + if (seconds == null) return '—'; + if (seconds < 60) return `${seconds.toFixed(1)}s`; + return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleString(undefined, { + dateStyle: 'short', + timeStyle: 'medium', + }); +} + +export default function AdminLogsPage() { + const { user } = useAuthStore(); + const router = useRouter(); + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState(''); + + useEffect(() => { + if (user && !user.is_superuser) { + router.replace('/dashboard'); + } + }, [user, router]); + + const { data, isLoading, isError, refetch } = useQuery({ + queryKey: ['admin-processing-runs', page, statusFilter], + queryFn: () => + adminApi.listProcessingRuns({ + page, + page_size: 25, + ...(statusFilter ? { status: statusFilter } : {}), + }), + enabled: !!user?.is_superuser, + }); + + return ( + + +
+ {/* Header */} +
+
+

+ + Activity Logs +

+

+ All email processing runs across every user account. +

+
+
+ + +
+
+ + {/* Table */} + {isLoading ? ( +
+
+
+ ) : isError ? ( +
+ Failed to load activity logs. Please try again. +
+ ) : data && data.items.length > 0 ? ( +
+
+ + + + + + + + + + + + + + + + {data.items.map((run: AdminProcessingRun) => { + const statusClass = + STATUS_STYLES[run.status] ?? 'bg-gray-100 text-gray-800'; + const ok = run.emails_failed === 0 && run.status !== 'failed'; + return ( + + + + + + + + + + + + ); + })} + +
StartedUserAccountStatusFetchedForwardedFailedDurationOK
+ {formatDate(run.started_at)} + + {run.user_email ?? `#${run.user_id}`} + +
{run.account_name ?? '—'}
+
{run.account_email ?? ''}
+
+ + {run.status} + + + {run.emails_fetched} + + {run.emails_forwarded} + + {run.emails_failed > 0 ? ( + {run.emails_failed} + ) : ( + 0 + )} + + {formatDuration(run.duration_seconds)} + + {ok ? ( + + ) : ( + + )} +
+
+ + {/* Pagination */} + {data.pages > 1 && ( +
+ + Page {data.page} of {data.pages} ({data.total} total runs) + +
+ + +
+
+ )} +
+ ) : ( +
+ +

No activity yet

+

+ Processing run logs will appear here once mail accounts are active. +

+
+ )} +
+ + + ); +} diff --git a/frontend/src/app/logs/page.tsx b/frontend/src/app/logs/page.tsx new file mode 100644 index 0000000..a591935 --- /dev/null +++ b/frontend/src/app/logs/page.tsx @@ -0,0 +1,293 @@ +'use client'; + +import { useState } from 'react'; +import { AuthGuard } from '@/components/AuthGuard'; +import { DashboardLayout } from '@/components/DashboardLayout'; +import { useQuery } from '@tanstack/react-query'; +import { processingRunsApi, ProcessingRun, ProcessingLog } from '@/lib/api'; +import { + FileText, + ChevronLeft, + ChevronRight, + CheckCircle, + XCircle, + Clock, + RefreshCw, + ChevronDown, + ChevronUp, +} from 'lucide-react'; + +const STATUS_STYLES: Record = { + completed: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + partial_failure: 'bg-yellow-100 text-yellow-800', + running: 'bg-blue-100 text-blue-800', +}; + +function formatDuration(seconds?: number | null): string { + if (seconds == null) return '—'; + if (seconds < 60) return `${seconds.toFixed(1)}s`; + return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleString(undefined, { + dateStyle: 'short', + timeStyle: 'medium', + }); +} + +function RunRow({ run }: { run: ProcessingRun }) { + const [expanded, setExpanded] = useState(false); + const [logsPage, setLogsPage] = useState(1); + + const { data: logsData, isLoading: logsLoading } = useQuery({ + queryKey: ['run-logs', run.id, logsPage], + queryFn: () => processingRunsApi.getLogs(run.id, { page: logsPage, page_size: 20 }), + enabled: expanded, + }); + + const statusClass = STATUS_STYLES[run.status] ?? 'bg-gray-100 text-gray-800'; + + return ( + <> + setExpanded((v) => !v)} + > + + {formatDate(run.started_at)} + + +
{run.account_name ?? '—'}
+
{run.account_email ?? ''}
+ + + + {run.status} + + + + {run.emails_fetched} + + + {run.emails_forwarded} + + + {run.emails_failed > 0 ? run.emails_failed : 0} + + + {formatDuration(run.duration_seconds)} + + + {expanded ? ( + + ) : ( + + )} + + + {expanded && ( + + + {run.error_message && ( +
+ {run.error_message} +
+ )} + {logsLoading ? ( +
+ Loading logs… +
+ ) : logsData && logsData.items.length > 0 ? ( + <> + + + + + + + + + + + + {logsData.items.map((log: ProcessingLog) => ( + + + + + + + + ))} + +
TimeLevelSubjectFromStatus
+ {formatDate(log.timestamp)} + + + {log.level} + + + {log.email_subject ?? '—'} + + {log.email_from ?? '—'} + + {log.success ? ( + + ) : ( + + )} +
+ {logsData.pages > 1 && ( +
+ + + Page {logsPage} of {logsData.pages} + + +
+ )} + + ) : ( +

No per-email logs recorded for this run.

+ )} + + + )} + + ); +} + +export default function LogsPage() { + const [page, setPage] = useState(1); + + const { data, isLoading, isError, refetch } = useQuery({ + queryKey: ['processing-runs', page], + queryFn: () => processingRunsApi.list({ page, page_size: 20 }), + }); + + return ( + + +
+ {/* Header */} +
+
+

+ + Processing Logs +

+

+ History of email processing runs for all your mail accounts. +

+
+ +
+ + {/* Table */} + {isLoading ? ( +
+
+
+ ) : isError ? ( +
+ Failed to load processing logs. Please try again. +
+ ) : data && data.items.length > 0 ? ( +
+
+ + + + + + + + + + + + + + {data.items.map((run) => ( + + ))} + +
StartedAccountStatusFetchedForwardedFailedDuration +
+
+ + {/* Pagination */} + {data.pages > 1 && ( +
+ + Page {data.page} of {data.pages} ({data.total} runs) + +
+ + +
+
+ )} +
+ ) : ( +
+ +

No processing runs yet

+

+ Logs will appear here once your mail accounts start processing emails. +

+
+ )} +
+ + + ); +}