diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
index 68c57e3..14ab996 100644
--- a/frontend/src/api/client.js
+++ b/frontend/src/api/client.js
@@ -119,6 +119,7 @@ function folderToRequest(slug, page, pageSize) {
}
export const EmailApi = {
+ get: (id) => api.get(`/email/${id}`).then((r) => r.data),
markRead: (id) => api.post(`/email/${id}/read`),
markUnread: (id) => api.post(`/email/${id}/unread`),
star: (id) => api.post(`/email/${id}/star`),
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index fa3c0f8..777883c 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from 'react';
import { AuthApi, SyncApi, AnalyticsApi } from '../api/client.js';
import Logo from './Logo.jsx';
import DevBanner from './DevBanner.jsx';
+import SyncStatus from './SyncStatus.jsx';
// ── Folder definitions ────────────────────────────────────────────────────────
@@ -236,6 +237,7 @@ export default function Layout() {
+
{user?.email}
diff --git a/frontend/src/components/SyncStatus.jsx b/frontend/src/components/SyncStatus.jsx
new file mode 100644
index 0000000..049cd28
--- /dev/null
+++ b/frontend/src/components/SyncStatus.jsx
@@ -0,0 +1,74 @@
+import { useEffect, useState } from 'react';
+import { SyncApi } from '../api/client.js';
+
+const POLL_INTERVAL_IDLE = 60_000; // 1 min when not syncing
+const POLL_INTERVAL_ACTIVE = 2_000; // 2 sec while running
+
+function fmtAge(iso) {
+ if (!iso) return null;
+ const mins = Math.floor((Date.now() - new Date(iso)) / 60000);
+ if (mins < 1) return 'just now';
+ if (mins < 60) return `${mins}m ago`;
+ const hrs = Math.floor(mins / 60);
+ if (hrs < 24) return `${hrs}h ago`;
+ return `${Math.floor(hrs / 24)}d ago`;
+}
+
+export default function SyncStatus() {
+ const [progress, setProgress] = useState(null);
+
+ useEffect(() => {
+ let timer;
+
+ const fetch = () => {
+ SyncApi.status().then((p) => {
+ setProgress(p);
+ const next = p.isRunning ? POLL_INTERVAL_ACTIVE : POLL_INTERVAL_IDLE;
+ timer = setTimeout(fetch, next);
+ }).catch(() => {
+ timer = setTimeout(fetch, POLL_INTERVAL_IDLE);
+ });
+ };
+
+ fetch();
+
+ // Also refresh when a sync is manually kicked off
+ const handler = () => { clearTimeout(timer); fetch(); };
+ window.addEventListener('inboxintel:sync-started', handler);
+
+ return () => { clearTimeout(timer); window.removeEventListener('inboxintel:sync-started', handler); };
+ }, []);
+
+ if (!progress) return null;
+
+ if (progress.isRunning) {
+ const pct = progress.total > 0
+ ? Math.round((progress.processed / progress.total) * 100)
+ : null;
+ return (
+
+
+
+ Syncing{pct !== null ? ` ${pct}%` : '…'}
+
+ {progress.total > 0 && (
+
+
+
+ )}
+
+ );
+ }
+
+ const age = fmtAge(progress.lastSuccessfulSyncUtc);
+ return (
+
+ {progress.lastError
+ ? ⚠ Sync error
+ : {age ? `Synced ${age}` : 'Never synced'}}
+
+ );
+}
diff --git a/frontend/src/pages/Senders.jsx b/frontend/src/pages/Senders.jsx
index 6f74d63..688b53d 100644
--- a/frontend/src/pages/Senders.jsx
+++ b/frontend/src/pages/Senders.jsx
@@ -65,7 +65,21 @@ function SenderList({ senders, selectedId, onSelect, search, onSearch }) {
// ── EmailDetail (full single-email view) ──────────────────────────────────────
-function EmailDetail({ email, onBack }) {
+function EmailDetail({ email: summary, onBack }) {
+ const [detail, setDetail] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setDetail(null);
+ setLoading(true);
+ EmailApi.get(summary.id)
+ .then(setDetail)
+ .catch(() => setDetail(null))
+ .finally(() => setLoading(false));
+ }, [summary.id]);
+
+ const email = detail ?? summary;
+
return (
@@ -83,9 +97,17 @@ function EmailDetail({ email, onBack }) {
)}
- {email.snippet && (
+
+ {loading && Loading message…
}
+
+ {!loading && detail?.bodyText && (
+ {detail.bodyText}
+ )}
+
+ {!loading && !detail?.bodyText && email.snippet && (
{email.snippet}
)}
+