Merge branch 'main' into copilot/add-apprise-alerting-capabilities
This commit is contained in:
@@ -351,7 +351,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"
|
||||
@@ -376,6 +376,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>
|
||||
|
||||
{/* System Alert Channels */}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const APP_NAME = process.env.APP_NAME ?? 'InboxConverge';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Datenschutz – POP3 Forwarder',
|
||||
title: `Datenschutz – ${APP_NAME}`,
|
||||
};
|
||||
|
||||
const LAST_UPDATED = 'March 26, 2026';
|
||||
const CONTACT_EMAIL = 'christianlouis@gmail.com';
|
||||
const CONTACT_EMAIL = process.env.CONTACT_EMAIL ?? 'christian@inboxconverge.com';
|
||||
|
||||
export default function DatenschutzPage() {
|
||||
return (
|
||||
@@ -82,7 +84,7 @@ export default function DatenschutzPage() {
|
||||
2. Geltungsbereich dieser Datenschutzerklärung
|
||||
</h2>
|
||||
<p className="text-gray-700">
|
||||
Dieser Hinweis gilt für die POP3 Forwarder-Webanwendung. Er gilt
|
||||
Dieser Hinweis gilt für die {APP_NAME}-Webanwendung. Er gilt
|
||||
für alle Nutzer weltweit, einschließlich derjenigen in der
|
||||
Europäischen Union (EU), dem Europäischen Wirtschaftsraum (EWR),
|
||||
Deutschland, dem Vereinigten Königreich (UK), der Schweiz, der
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const APP_NAME = process.env.APP_NAME ?? 'InboxConverge';
|
||||
const CONTACT_EMAIL = process.env.CONTACT_EMAIL ?? 'christian@inboxconverge.com';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Impressum – POP3 Forwarder',
|
||||
title: `Impressum – ${APP_NAME}`,
|
||||
};
|
||||
|
||||
export default function ImpressumPage() {
|
||||
@@ -36,10 +39,10 @@ export default function ImpressumPage() {
|
||||
Fax: +49 40 97074609<br />
|
||||
E-Mail:{' '}
|
||||
<a
|
||||
href="mailto:christianlouis@gmail.com"
|
||||
href={`mailto:${CONTACT_EMAIL}`}
|
||||
className="text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
christianlouis@gmail.com
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -14,7 +14,7 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "InboxRescue — your old inboxes, delivered to Gmail",
|
||||
title: "InboxConverge — your old inboxes, delivered to Gmail",
|
||||
description: "Poll your legacy POP3 and IMAP mailboxes and have everything land quietly in Gmail. Set it once, forget it exists.",
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function LoginPage() {
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
POP3 to Gmail Forwarder
|
||||
InboxConverge
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Sign in to your account
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function Home() {
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex items-center">
|
||||
<Mail className="h-8 w-8 text-blue-600 mr-2" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">InboxRescue</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900">InboxConverge</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
@@ -136,7 +136,7 @@ export default function Home() {
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-4 max-w-2xl mx-auto">
|
||||
You know the ones — that GMX account from 2009, the old ISP address your
|
||||
bank still sends to, the Hotmail you gave out in school. InboxRescue
|
||||
bank still sends to, the Hotmail you gave out in school. InboxConverge
|
||||
quietly polls them all and drops everything into your Gmail. Set it once,
|
||||
forget it exists.
|
||||
</p>
|
||||
@@ -171,7 +171,7 @@ export default function Home() {
|
||||
Auto-detects everything
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Type your old email address and InboxRescue figures out the server
|
||||
Type your old email address and InboxConverge figures out the server
|
||||
settings. No Googling port numbers required.
|
||||
</p>
|
||||
</div>
|
||||
@@ -215,7 +215,7 @@ export default function Home() {
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold text-gray-900 mb-2">Add your old inbox</h4>
|
||||
<p className="text-gray-600">
|
||||
Paste the email address — InboxRescue auto-detects the POP3/IMAP
|
||||
Paste the email address — InboxConverge auto-detects the POP3/IMAP
|
||||
settings in seconds.
|
||||
</p>
|
||||
</div>
|
||||
@@ -225,7 +225,7 @@ export default function Home() {
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold text-gray-900 mb-2">Connect Gmail</h4>
|
||||
<p className="text-gray-600">
|
||||
Sign in with Google once. InboxRescue delivers mail directly into
|
||||
Sign in with Google once. InboxConverge delivers mail directly into
|
||||
your inbox using the Gmail API — no SMTP relay needed.
|
||||
</p>
|
||||
</div>
|
||||
@@ -270,7 +270,7 @@ export default function Home() {
|
||||
<footer className="mt-20 border-t border-gray-200 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<p className="text-center text-gray-600 text-sm">
|
||||
© {new Date().getFullYear()} InboxRescue — made for people, not enterprises.
|
||||
© {new Date().getFullYear()} InboxConverge — made for people, not enterprises.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -16,8 +16,112 @@ import {
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Bug,
|
||||
RotateCcw,
|
||||
Tags,
|
||||
} from 'lucide-react';
|
||||
|
||||
const DEFAULT_GMAIL_IMPORT_LABEL_TEMPLATES = ['{{source_email}}', 'imported'];
|
||||
|
||||
function parseImportLabelTemplates(input: string): string[] {
|
||||
return input
|
||||
.split('\n')
|
||||
.map((value) => value.trim())
|
||||
.filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
|
||||
}
|
||||
|
||||
function GmailImportLabelsForm({
|
||||
gmailCredential,
|
||||
onSave,
|
||||
isSaving,
|
||||
}: {
|
||||
gmailCredential: {
|
||||
gmail_email: string;
|
||||
import_label_templates: string[];
|
||||
default_import_label_templates: string[];
|
||||
};
|
||||
onSave: (labels: string[]) => void;
|
||||
isSaving: boolean;
|
||||
}) {
|
||||
const [labelsInput, setLabelsInput] = useState(
|
||||
gmailCredential.import_label_templates.join('\n')
|
||||
);
|
||||
|
||||
const defaultTemplates =
|
||||
gmailCredential.default_import_label_templates.length > 0
|
||||
? gmailCredential.default_import_label_templates
|
||||
: DEFAULT_GMAIL_IMPORT_LABEL_TEMPLATES;
|
||||
const parsedLabels = parseImportLabelTemplates(labelsInput);
|
||||
const isDefaultSelection =
|
||||
parsedLabels.length === defaultTemplates.length &&
|
||||
parsedLabels.every((value, index) => value === defaultTemplates[index]);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Tags className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="text-sm font-semibold text-gray-900">Import labels</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
One label is created per line. We recommend keeping{' '}
|
||||
<code className="rounded bg-white px-1 py-0.5 text-xs text-gray-700">
|
||||
{'{{source_email}}'}
|
||||
</code>{' '}
|
||||
so each imported message is tagged with the mailbox it came from, plus a
|
||||
catch-all label like <strong>imported</strong>.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Example: a mail pulled from <strong>billing@example.com</strong> will be
|
||||
labeled as <strong>billing@example.com</strong> when{' '}
|
||||
<code className="rounded bg-white px-1 py-0.5 text-xs text-gray-700">
|
||||
{'{{source_email}}'}
|
||||
</code>{' '}
|
||||
is present.
|
||||
</p>
|
||||
<textarea
|
||||
value={labelsInput}
|
||||
onChange={(e) => setLabelsInput(e.target.value)}
|
||||
rows={4}
|
||||
className="mt-4 w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={`{{source_email}}\nimported`}
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||
<span>Suggested defaults:</span>
|
||||
{defaultTemplates.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="rounded-full border border-gray-200 bg-white px-2 py-1 text-gray-700"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSave(parsedLabels)}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
Save label setup
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLabelsInput(defaultTemplates.join('\n'))}
|
||||
disabled={isSaving || isDefaultSelection}
|
||||
className="flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm text-gray-700 ring-1 ring-gray-300 transition-colors hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Reset defaults
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-gray-500">
|
||||
Connected Gmail target: <strong>{gmailCredential.gmail_email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
@@ -106,6 +210,7 @@ function SettingsContent() {
|
||||
});
|
||||
|
||||
const [debugEmailResult, setDebugEmailResult] = useState<string | null>(null);
|
||||
const [gmailLabelsSaved, setGmailLabelsSaved] = useState(false);
|
||||
const sendDebugEmailMutation = useMutation({
|
||||
mutationFn: gmailApi.sendDebugEmail,
|
||||
onSuccess: () => {
|
||||
@@ -118,6 +223,15 @@ function SettingsContent() {
|
||||
},
|
||||
});
|
||||
|
||||
const updateGmailLabelsMutation = useMutation({
|
||||
mutationFn: gmailApi.updateImportLabels,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['gmail-credential'] });
|
||||
setGmailLabelsSaved(true);
|
||||
setTimeout(() => setGmailLabelsSaved(false), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const saveSmtpMutation = useMutation({
|
||||
mutationFn: smtpApi.save,
|
||||
onSuccess: () => {
|
||||
@@ -362,7 +476,9 @@ function SettingsContent() {
|
||||
{debugEmailResult === 'success' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md px-3 py-2">
|
||||
<CheckCircle className="h-4 w-4 flex-shrink-0" />
|
||||
Debug email injected successfully. Check your Gmail inbox — it should be labelled <strong className="mx-1">test</strong> and <strong className="mx-1">imported</strong>.
|
||||
Debug email injected successfully. Check your Gmail inbox for the
|
||||
configured import labels plus a <strong className="mx-1">test</strong>{' '}
|
||||
label.
|
||||
</div>
|
||||
)}
|
||||
{debugEmailResult === 'error' && (
|
||||
@@ -371,6 +487,18 @@ function SettingsContent() {
|
||||
Failed to inject debug email. Check that Gmail API access is still valid.
|
||||
</div>
|
||||
)}
|
||||
<GmailImportLabelsForm
|
||||
key={gmailCredential.updated_at}
|
||||
gmailCredential={gmailCredential}
|
||||
isSaving={updateGmailLabelsMutation.isPending}
|
||||
onSave={(labels) => updateGmailLabelsMutation.mutate(labels)}
|
||||
/>
|
||||
{gmailLabelsSaved && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md px-3 py-2">
|
||||
<CheckCircle className="h-4 w-4 flex-shrink-0" />
|
||||
Gmail import labels saved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -589,4 +717,3 @@ function SettingsContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
Users,
|
||||
CreditCard,
|
||||
Bell
|
||||
FileText,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
@@ -37,6 +39,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Mail Accounts', href: '/accounts', icon: Mail },
|
||||
{ name: 'Notifications', href: '/notifications', icon: Bell },
|
||||
{ name: 'Logs', href: '/logs', icon: FileText },
|
||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
@@ -45,6 +48,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 },
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -56,7 +60,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
|
||||
<div className="flex flex-col flex-grow bg-white border-r border-gray-200">
|
||||
<div className="flex items-center h-16 flex-shrink-0 px-4 border-b border-gray-200">
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxRescue</h1>
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxConverge</h1>
|
||||
</div>
|
||||
<nav className="flex-1 px-2 py-4 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
@@ -119,7 +123,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<div className="fixed inset-0 bg-gray-600/75" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-white">
|
||||
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxRescue</h1>
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxConverge</h1>
|
||||
<button onClick={() => setSidebarOpen(false)} className="text-gray-500 hover:text-gray-700">
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
+143
-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 {
|
||||
@@ -141,6 +199,8 @@ export interface GmailCredential {
|
||||
user_id: number;
|
||||
gmail_email: string;
|
||||
is_valid: boolean;
|
||||
import_label_templates: string[];
|
||||
default_import_label_templates: string[];
|
||||
last_verified_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -292,10 +352,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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -336,6 +440,14 @@ export const gmailApi = {
|
||||
const response = await api.post<GmailDebugEmailResponse>('/providers/gmail/debug-email');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** Update the labels applied to imported Gmail messages. */
|
||||
async updateImportLabels(importLabelTemplates: string[]): Promise<GmailCredential> {
|
||||
const response = await api.put<GmailCredential>('/providers/gmail-credential/labels', {
|
||||
import_label_templates: importLabelTemplates,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ── SMTP Config API ─────────────────────────────────────────────────────
|
||||
@@ -483,6 +595,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;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Notification Types ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user