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:
sysops
2026-03-17 02:17:44 +01:00
parent 9cc540a880
commit 988c37d85d
9 changed files with 762 additions and 52 deletions
+95 -7
View File
@@ -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
+31
View File
@@ -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" },
];
+18
View File
@@ -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 {