feat(PROJ-8): Automatischer IMAP-Sync (Cron-Scheduler)
Backend:
- internal/imap/store.go: 7 neue Felder (sync_interval_min, last_sync_at,
last_sync_count, last_uid, sync_running, sync_status, sync_error_msg)
DB-Migration via ALTER TABLE ADD COLUMN IF NOT EXISTS
Neue Methoden: ListAll, UpdateSyncInterval, SetSyncRunning, UpdateSyncResult
- internal/imap/scheduler.go: Scheduler mit time.Ticker (1 min),
inkrementeller Sync via UID SEARCH UID <lastUID+1>:*,
exponential backoff (3 Versuche: 1s / 60s / 300s),
sync_running-Flag verhindert parallele Syncs
- internal/api/server.go: POST /api/imap/{id}/sync (manueller Trigger),
PATCH /api/imap/{id} (sync_interval_min setzen, 0 oder 5-1440 min)
- cmd/archivmail/main.go: Scheduler gestartet + via SetImap verdrahtet
Frontend:
- src/lib/api.ts: 6 neue ImapAccount-Felder, triggerImapSync, updateImapInterval
- src/app/imap/page.tsx: Intervall-Dropdown, "Sync jetzt"-Button,
Letzter-Sync-Anzeige mit Status-Badge, Polling auch bei sync_running
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+95
-7
@@ -10,6 +10,8 @@ import {
|
||||
testImapConnection,
|
||||
startImapImport,
|
||||
getImapProgress,
|
||||
triggerImapSync,
|
||||
updateImapInterval,
|
||||
type ImapAccount,
|
||||
type ImapFolder,
|
||||
} from "@/lib/api";
|
||||
@@ -83,17 +85,18 @@ export default function ImapPage() {
|
||||
if (user) loadAccounts();
|
||||
}, [user, loadAccounts]);
|
||||
|
||||
// Start polling for running accounts
|
||||
// Start polling for running accounts (import or sync)
|
||||
useEffect(() => {
|
||||
for (const acc of accounts) {
|
||||
if (acc.status === "running" && !pollingRefs.current.has(acc.id)) {
|
||||
const isActive = acc.status === "running" || acc.sync_running;
|
||||
if (isActive && !pollingRefs.current.has(acc.id)) {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const updated = await getImapProgress(acc.id);
|
||||
setAccounts((prev) =>
|
||||
prev.map((a) => (a.id === updated.id ? updated : a))
|
||||
);
|
||||
if (updated.status !== "running") {
|
||||
if (updated.status !== "running" && !updated.sync_running) {
|
||||
clearInterval(pollingRefs.current.get(acc.id)!);
|
||||
pollingRefs.current.delete(acc.id);
|
||||
}
|
||||
@@ -105,10 +108,10 @@ export default function ImapPage() {
|
||||
pollingRefs.current.set(acc.id, interval);
|
||||
}
|
||||
}
|
||||
// Cleanup intervals for accounts that are no longer running
|
||||
// Cleanup intervals for accounts that are no longer active
|
||||
for (const [id, interval] of pollingRefs.current) {
|
||||
const acc = accounts.find((a) => a.id === id);
|
||||
if (!acc || acc.status !== "running") {
|
||||
if (!acc || (acc.status !== "running" && !acc.sync_running)) {
|
||||
clearInterval(interval);
|
||||
pollingRefs.current.delete(id);
|
||||
}
|
||||
@@ -206,6 +209,25 @@ export default function ImapPage() {
|
||||
setDeleteConfirm(null);
|
||||
}
|
||||
|
||||
async function handleSyncNow(id: number) {
|
||||
try {
|
||||
const updated = await triggerImapSync(id);
|
||||
setAccounts((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
|
||||
} catch {
|
||||
// ignore — conflict means sync already running
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIntervalChange(id: number, value: string) {
|
||||
const intervalMin = parseInt(value, 10);
|
||||
try {
|
||||
const updated = await updateImapInterval(id, intervalMin);
|
||||
setAccounts((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExcluded(folderName: string) {
|
||||
setExcludedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -229,6 +251,20 @@ export default function ImapPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function syncBadge(acc: ImapAccount) {
|
||||
if (acc.sync_running) {
|
||||
return <Badge className="bg-blue-500 text-white">Sync laeuft...</Badge>;
|
||||
}
|
||||
if (!acc.sync_status) return null;
|
||||
if (acc.sync_status === "ok") {
|
||||
return <Badge className="bg-green-600 text-white">Sync OK</Badge>;
|
||||
}
|
||||
if (acc.sync_status === "error") {
|
||||
return <Badge variant="destructive">Sync Fehler</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
|
||||
@@ -305,13 +341,57 @@ export default function ImapPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* PROJ-8: Sync status */}
|
||||
{acc.last_sync_at && (
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Letzter Sync:{" "}
|
||||
{new Date(acc.last_sync_at).toLocaleString("de-DE")} (
|
||||
{acc.last_sync_count} neu)
|
||||
</p>
|
||||
{syncBadge(acc)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{acc.sync_running && !acc.last_sync_at && syncBadge(acc)}
|
||||
|
||||
{acc.sync_error_msg && acc.sync_status === "error" && (
|
||||
<p className="mb-3 text-sm text-destructive">
|
||||
Sync-Fehler: {acc.sync_error_msg}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{acc.excluded_folders && acc.excluded_folders.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Ausgeschlossene Ordner: {acc.excluded_folders.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* PROJ-8: Sync interval selector */}
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
Auto-Sync:
|
||||
</span>
|
||||
<Select
|
||||
value={String(acc.sync_interval_min ?? 0)}
|
||||
onValueChange={(v) => handleIntervalChange(acc.id, v)}
|
||||
>
|
||||
<SelectTrigger className="w-40 h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">Deaktiviert</SelectItem>
|
||||
<SelectItem value="5">5 min</SelectItem>
|
||||
<SelectItem value="15">15 min</SelectItem>
|
||||
<SelectItem value="30">30 min</SelectItem>
|
||||
<SelectItem value="60">1 Stunde</SelectItem>
|
||||
<SelectItem value="360">6 Stunden</SelectItem>
|
||||
<SelectItem value="1440">24 Stunden</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={acc.status === "running"}
|
||||
@@ -319,10 +399,18 @@ export default function ImapPage() {
|
||||
>
|
||||
Import starten
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={acc.status === "running" || acc.sync_running}
|
||||
onClick={() => handleSyncNow(acc.id)}
|
||||
>
|
||||
Sync jetzt
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={acc.status === "running"}
|
||||
disabled={acc.status === "running" || acc.sync_running}
|
||||
onClick={() => setDeleteConfirm(acc.id)}
|
||||
>
|
||||
Loeschen
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export type FeatureStatus = "Planned" | "In Progress" | "In Review" | "Deployed";
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
name: string;
|
||||
status: FeatureStatus;
|
||||
frontend: boolean;
|
||||
backend: boolean;
|
||||
lastUpdated: string; // YYYY-MM-DD
|
||||
}
|
||||
|
||||
export const features: Feature[] = [
|
||||
{ id: "PROJ-1", name: "Authentifizierung & Rollen", status: "In Review", frontend: true, backend: true, lastUpdated: "2026-03-15" },
|
||||
{ id: "PROJ-2", name: "Import: EML/MBOX Upload", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-3", name: "Import: IMAP-Verbindung", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-4", name: "Import: SMTP-Eingang via BCC", status: "In Progress", frontend: false, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-5", name: "Speicherung & Volltext-Indexierung", status: "In Review", frontend: false, backend: true, lastUpdated: "2026-03-14" },
|
||||
{ id: "PROJ-6", name: "Volltext-Suche & Filterung", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-7", name: "E-Mail-Ansicht (Lesen & Anhänge)", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-8", name: "Automatischer IMAP-Sync (Cron-Job)", status: "In Progress", frontend: false, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-9", name: "Ordner- & Label-Verwaltung", status: "In Progress", frontend: false, backend: false, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-10", name: "Admin-Bereich: Nutzer- & Postfachverw.", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-11", name: "Audit-Log & Compliance-Berichte", status: "In Progress", frontend: true, backend: true, lastUpdated: "2026-03-12" },
|
||||
{ id: "PROJ-12", name: "E-Mail-Export (EML/PDF/ZIP)", status: "In Review", frontend: true, backend: true, lastUpdated: "2026-03-13" },
|
||||
{ id: "PROJ-13", name: "REST API für externe CRM-Anbindung", status: "In Progress", frontend: false, backend: true, lastUpdated: "2026-03-13" },
|
||||
{ id: "PROJ-14", name: "Import: POP3-Verbindung", status: "In Progress", frontend: false, backend: false, lastUpdated: "2026-03-13" },
|
||||
{ id: "PROJ-15", name: "CLI Import & Export", status: "In Review", frontend: false, backend: true, lastUpdated: "2026-03-13" },
|
||||
{ id: "PROJ-16", name: "LDAP / Active Directory Anbindung", status: "In Progress", frontend: false, backend: false, lastUpdated: "2026-03-13" },
|
||||
{ id: "PROJ-17", name: "Admin Dashboard – Systemauslastung", status: "In Review", frontend: true, backend: true, lastUpdated: "2026-03-14" },
|
||||
{ id: "PROJ-18", name: "E-Mail Integritätsprüfung", status: "In Review", frontend: true, backend: true, lastUpdated: "2026-03-14" },
|
||||
];
|
||||
@@ -314,6 +314,13 @@ export interface ImapAccount {
|
||||
progress_current: number;
|
||||
progress_total: number;
|
||||
created_at: string;
|
||||
// PROJ-8: Auto-sync fields
|
||||
sync_interval_min: number;
|
||||
last_sync_at?: string;
|
||||
last_sync_count: number;
|
||||
sync_running: boolean;
|
||||
sync_status: string;
|
||||
sync_error_msg: string;
|
||||
}
|
||||
|
||||
export interface ImapTestResult {
|
||||
@@ -366,6 +373,17 @@ export async function getImapProgress(id: number): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}/progress`);
|
||||
}
|
||||
|
||||
export async function triggerImapSync(id: number): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}/sync`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function updateImapInterval(id: number, intervalMin: number): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ sync_interval_min: intervalMin }),
|
||||
});
|
||||
}
|
||||
|
||||
// ── System Stats ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface SystemStatsCPU {
|
||||
|
||||
Reference in New Issue
Block a user