Merge pull request #73 from christianlouis/copilot/fix-wizard-account-creation

Fix account creation wizard, missing required form fields, and implement Settings page
This commit is contained in:
Christian Krakau-Louis
2026-03-25 11:14:21 +01:00
committed by GitHub
7 changed files with 315 additions and 59 deletions
+5
View File
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed ### Fixed
- Fixed three ESLint errors that caused CI to fail: removed unused `_setUser` store binding and unused `useAuthStore` import from `login/page.tsx`; replaced unused `_err` catch binding with a bare `catch {}` in `login/page.tsx`; removed a `useEffect` in `settings/page.tsx` that called `setProfileForm` synchronously (flagged by `react-hooks/set-state-in-effect`) — the effect was redundant because `useState` already initialises the form from the auth store's `user` object, which is the same value passed as `initialData` to `useQuery`.
- Fixed wizard to create new mail accounts showing a big grey screen: `bg-opacity-75` was removed in Tailwind CSS v4; replaced with the `/75` opacity modifier syntax (`bg-gray-500/75`) in `AddMailAccountModal` and `DashboardLayout` mobile overlay. Restructured the modal from the deprecated `inline-block align-bottom` centering trick to a proper flexbox layout with `relative z-10` on the modal content.
- Fixed mail account creation always failing with a backend validation error: `email_address` and `forward_to` are required fields in the backend schema but were missing from the `AddMailAccountModal` form. Added both fields to the form — `email_address` is auto-synced from the username input, and `forward_to` (destination Gmail address) is a new explicit field pre-populated from the logged-in user's email. Also added `delivery_method` selector and `delete_after_forward` checkbox.
- Implemented the Settings page (was a placeholder showing "coming soon"): now includes a Profile section to update name and email via `PUT /users/me`, an Account Information section showing subscription tier/status and member-since date, and a Security section.
- Fixed `sqlalchemy.exc.DBAPIError` raised by asyncpg when inserting timezone-aware `datetime.now(timezone.utc)` values into timezone-naive `DateTime` (TIMESTAMP WITHOUT TIME ZONE) columns: changed all `DateTime` column definitions in `database_models.py` to `DateTime(timezone=True)` (TIMESTAMP WITH TIME ZONE) and replaced all `default=datetime.utcnow` callable references with `default=lambda: datetime.now(timezone.utc)` for consistent, timezone-aware timestamps throughout the ORM. - Fixed `sqlalchemy.exc.DBAPIError` raised by asyncpg when inserting timezone-aware `datetime.now(timezone.utc)` values into timezone-naive `DateTime` (TIMESTAMP WITHOUT TIME ZONE) columns: changed all `DateTime` column definitions in `database_models.py` to `DateTime(timezone=True)` (TIMESTAMP WITH TIME ZONE) and replaced all `default=datetime.utcnow` callable references with `default=lambda: datetime.now(timezone.utc)` for consistent, timezone-aware timestamps throughout the ORM.
- Fixed `ProgrammingError` (`cached statement plan is invalid`) raised by the asyncpg dialect during startup: SQLAlchemy's asyncpg wrapper maintains an LRU prepared-statement cache per connection (default size 100). When `Base.metadata.create_all()` executes `CREATE TYPE … AS ENUM` DDL inside a transaction, PostgreSQL invalidates the cached plans for that connection. The next enum-type existence check then fails because the dialect tries to reuse the now-stale prepared statement. Fix: set `prepared_statement_cache_size=0` in `connect_args` on `create_async_engine` to disable the cache entirely, which is the documented SQLAlchemy recommendation for DDL-at-startup scenarios. - Fixed `ProgrammingError` (`cached statement plan is invalid`) raised by the asyncpg dialect during startup: SQLAlchemy's asyncpg wrapper maintains an LRU prepared-statement cache per connection (default size 100). When `Base.metadata.create_all()` executes `CREATE TYPE … AS ENUM` DDL inside a transaction, PostgreSQL invalidates the cached plans for that connection. The next enum-type existence check then fails because the dialect tries to reuse the now-stale prepared statement. Fix: set `prepared_statement_cache_size=0` in `connect_args` on `create_async_engine` to disable the cache entirely, which is the documented SQLAlchemy recommendation for DDL-at-startup scenarios.
- Fixed `UndefinedTableError` on first boot: the lifespan startup event now calls `Base.metadata.create_all()` via the async engine before attempting to seed default settings, so all tables are created automatically when the database is empty (e.g., fresh PostgreSQL container with no Alembic migrations run yet). - Fixed `UndefinedTableError` on first boot: the lifespan startup event now calls `Base.metadata.create_all()` via the async engine before attempting to seed default settings, so all tables are created automatically when the database is empty (e.g., fresh PostgreSQL container with no Alembic migrations run yet).
@@ -28,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded `sqlalchemy` from `2.0.25` to `2.0.48` to fix `AssertionError: Class ... directly inherits TypingOnly but has additional attributes` on Python 3.14 (`__static_attributes__`, `__firstlineno__`) - Upgraded `sqlalchemy` from `2.0.25` to `2.0.48` to fix `AssertionError: Class ... directly inherits TypingOnly but has additional attributes` on Python 3.14 (`__static_attributes__`, `__firstlineno__`)
### Added ### Added
- `userApi.updateProfile()` method in `frontend/src/lib/api.ts` for updating user profile via `PUT /users/me`
- **Backend URL logged at startup**: The Next.js server now logs the resolved `BACKEND_URL` (e.g. `[proxy] BACKEND_URL = http://backend:8000`) via `src/instrumentation.ts` when the server starts, making it easy to diagnose `ECONNREFUSED` proxy errors. The per-request error log now also includes the full target URL. - **Backend URL logged at startup**: The Next.js server now logs the resolved `BACKEND_URL` (e.g. `[proxy] BACKEND_URL = http://backend:8000`) via `src/instrumentation.ts` when the server starts, making it easy to diagnose `ECONNREFUSED` proxy errors. The per-request error log now also includes the full target URL.
- **Dual-registry Docker deployment**: CI now builds separate backend and frontend images and pushes to both GHCR (`ghcr.io`) and private registry (`registry.cklnet.com`) using a matrix strategy - **Dual-registry Docker deployment**: CI now builds separate backend and frontend images and pushes to both GHCR (`ghcr.io`) and private registry (`registry.cklnet.com`) using a matrix strategy
- **Database-backed configuration**: `AppSetting` model and `ConfigService` for hybrid config (DB-first, env-var fallback) - **Database-backed configuration**: `AppSetting` model and `ConfigService` for hybrid config (DB-first, env-var fallback)
+3 -2
View File
@@ -209,9 +209,10 @@ because the API client layer is missing.
- [x] Dashboard with stats cards and processing runs table - [x] Dashboard with stats cards and processing runs table
- [x] Mail accounts list with CRUD operations - [x] Mail accounts list with CRUD operations
- [x] Settings page - [x] Settings page
- [x] `AddMailAccountModal` component (auto-detect, test connection) - [x] `AddMailAccountModal` component (auto-detect, test connection, all required fields)
- [x] `DashboardLayout` with responsive sidebar - [x] `DashboardLayout` with responsive sidebar
- [x] `AuthGuard` for protected routes - [x] `AuthGuard` for protected routes
- [x] Fix wizard grey screen (Tailwind v4 `bg-opacity``/75` syntax, modal restructure)
### Not Started 📋 ### Not Started 📋
- [ ] End-to-end testing of frontend against backend API - [ ] End-to-end testing of frontend against backend API
@@ -307,7 +308,7 @@ because the API client layer is missing.
| Production Ready | 30% | 🔴 Needs Work | | Production Ready | 30% | 🔴 Needs Work |
| Observability | 10% | 🔴 Needs Work | | Observability | 10% | 🔴 Needs Work |
| Backend Features | 85% | 🟢 Near Complete | | Backend Features | 85% | 🟢 Near Complete |
| Frontend | 50% | 🟡 In Progress | | Frontend | 65% | 🟢 Near Complete |
**Overall Repository Readiness**: 55% ⚠️ **Overall Repository Readiness**: 55% ⚠️
+1 -3
View File
@@ -4,11 +4,9 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { authApi } from '@/lib/api'; import { authApi } from '@/lib/api';
import { useAuthStore } from '@/store/authStore';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const _setUser = useAuthStore((state) => state.setUser);
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -38,7 +36,7 @@ export default function LoginPage() {
const redirectUri = `${window.location.origin}/auth/callback`; const redirectUri = `${window.location.origin}/auth/callback`;
const authUrl = await authApi.getGoogleAuthUrl(redirectUri); const authUrl = await authApi.getGoogleAuthUrl(redirectUri);
window.location.href = authUrl; window.location.href = authUrl;
} catch (_err: unknown) { } catch {
setError('Failed to initialize Google login'); setError('Failed to initialize Google login');
} }
}; };
+193 -6
View File
@@ -1,19 +1,206 @@
'use client'; 'use client';
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AuthGuard } from '@/components/AuthGuard'; import { AuthGuard } from '@/components/AuthGuard';
import { DashboardLayout } from '@/components/DashboardLayout'; import { DashboardLayout } from '@/components/DashboardLayout';
import { userApi } from '@/lib/api';
import { useAuthStore } from '@/store/authStore';
import { CheckCircle, Loader2, User, Mail, Shield } from 'lucide-react';
export default function SettingsPage() { export default function SettingsPage() {
return ( return (
<AuthGuard> <AuthGuard>
<DashboardLayout> <DashboardLayout>
<div className="space-y-6"> <SettingsContent />
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-gray-600">Settings page coming soon...</p>
</div>
</div>
</DashboardLayout> </DashboardLayout>
</AuthGuard> </AuthGuard>
); );
} }
function SettingsContent() {
const queryClient = useQueryClient();
const { user, setUser } = useAuthStore();
const [profileForm, setProfileForm] = useState({
full_name: user?.full_name || '',
email: user?.email || '',
});
const [profileSaved, setProfileSaved] = useState(false);
// Refresh user data from the server
const { data: currentUser } = useQuery({
queryKey: ['current-user'],
queryFn: userApi.getCurrentUser,
initialData: user ?? undefined,
});
const updateProfileMutation = useMutation({
mutationFn: (data: { full_name: string; email: string }) =>
userApi.updateProfile(data),
onSuccess: (updatedUser) => {
setUser(updatedUser);
queryClient.invalidateQueries({ queryKey: ['current-user'] });
setProfileSaved(true);
setTimeout(() => setProfileSaved(false), 3000);
},
});
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setProfileForm((prev) => ({ ...prev, [name]: value }));
};
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await updateProfileMutation.mutateAsync({
full_name: profileForm.full_name,
email: profileForm.email,
});
} catch (error) {
const errorMessage =
error instanceof Error && 'response' in error
? (error as { response?: { data?: { detail?: string } } }).response?.data
?.detail
: null;
alert(errorMessage || 'Failed to update profile');
}
};
const displayUser = currentUser ?? user;
return (
<div className="space-y-8">
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
{/* Profile Section */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
<User className="h-5 w-5 text-gray-500" />
<h2 className="text-lg font-semibold text-gray-900">Profile</h2>
</div>
<form onSubmit={handleProfileSubmit} className="px-6 py-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
type="text"
name="full_name"
value={profileForm.full_name}
onChange={handleProfileChange}
className="w-full max-w-md px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Your full name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
name="email"
value={profileForm.email}
onChange={handleProfileChange}
className="w-full max-w-md px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="you@example.com"
/>
</div>
<div className="flex items-center gap-3 pt-2">
<button
type="submit"
disabled={updateProfileMutation.isPending}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{updateProfileMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Saving
</>
) : (
'Save Changes'
)}
</button>
{profileSaved && (
<span className="flex items-center text-sm text-green-600">
<CheckCircle className="h-4 w-4 mr-1" />
Saved successfully
</span>
)}
</div>
</form>
</div>
{/* Account Information */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
<Mail className="h-5 w-5 text-gray-500" />
<h2 className="text-lg font-semibold text-gray-900">Account Information</h2>
</div>
<div className="px-6 py-6 space-y-3">
<div className="flex items-center text-sm">
<span className="text-gray-500 w-40">Subscription tier:</span>
<span className="font-medium text-gray-900 capitalize">
{displayUser?.subscription_tier ?? '—'}
</span>
</div>
<div className="flex items-center text-sm">
<span className="text-gray-500 w-40">Subscription status:</span>
<span className="font-medium text-gray-900 capitalize">
{displayUser?.subscription_status ?? '—'}
</span>
</div>
<div className="flex items-center text-sm">
<span className="text-gray-500 w-40">Member since:</span>
<span className="text-gray-900">
{displayUser?.created_at
? new Date(displayUser.created_at).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: '—'}
</span>
</div>
{displayUser?.oauth_provider && (
<div className="flex items-center text-sm">
<span className="text-gray-500 w-40">Linked account:</span>
<span className="font-medium text-gray-900 capitalize">
{displayUser.oauth_provider}
</span>
</div>
)}
{displayUser?.last_login_at && (
<div className="flex items-center text-sm">
<span className="text-gray-500 w-40">Last login:</span>
<span className="text-gray-900">
{new Date(displayUser.last_login_at).toLocaleString()}
</span>
</div>
)}
</div>
</div>
{/* Security Section */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
<Shield className="h-5 w-5 text-gray-500" />
<h2 className="text-lg font-semibold text-gray-900">Security</h2>
</div>
<div className="px-6 py-6">
<p className="text-sm text-gray-600">
Password change and two-factor authentication settings are coming soon.
</p>
{displayUser?.oauth_provider === 'google' && (
<p className="mt-2 text-sm text-gray-500">
Your account is authenticated via Google OAuth password management is handled by Google.
</p>
)}
</div>
</div>
</div>
);
}
+105 -45
View File
@@ -3,6 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { mailAccountsApi, MailAccount, MailAccountCreate } from '@/lib/api'; import { mailAccountsApi, MailAccount, MailAccountCreate } from '@/lib/api';
import { useAuthStore } from '@/store/authStore';
import { X, Loader2, CheckCircle, XCircle } from 'lucide-react'; import { X, Loader2, CheckCircle, XCircle } from 'lucide-react';
import { ProviderWizard } from './ProviderWizard'; import { ProviderWizard } from './ProviderWizard';
@@ -15,6 +16,7 @@ type WizardStep = 'provider' | 'form';
export function AddMailAccountModal({ account, onClose }: AddMailAccountModalProps) { export function AddMailAccountModal({ account, onClose }: AddMailAccountModalProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { user } = useAuthStore();
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle'); const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
const [testMessage, setTestMessage] = useState(''); const [testMessage, setTestMessage] = useState('');
const [autoDetecting, setAutoDetecting] = useState(false); const [autoDetecting, setAutoDetecting] = useState(false);
@@ -22,14 +24,20 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
const [formData, setFormData] = useState<MailAccountCreate>({ const [formData, setFormData] = useState<MailAccountCreate>({
name: account?.name || '', name: account?.name || '',
protocol: account?.protocol || 'pop3', email_address: account?.email_address || '',
protocol: account?.protocol || 'pop3_ssl',
host: account?.host || '', host: account?.host || '',
port: account?.port || 995, port: account?.port || 995,
username: '', username: '',
password: '', password: '',
use_ssl: account?.use_ssl ?? true, use_ssl: account?.use_ssl ?? true,
use_tls: account?.use_tls ?? false,
forward_to: account?.forward_to || user?.email || '',
delivery_method: account?.delivery_method || 'gmail_api',
is_enabled: account?.is_enabled ?? true,
check_interval_minutes: account?.check_interval_minutes || 5, check_interval_minutes: account?.check_interval_minutes || 5,
max_emails_per_check: account?.max_emails_per_check || 100, max_emails_per_check: account?.max_emails_per_check || 50,
delete_after_forward: account?.delete_after_forward ?? true,
}); });
const createMutation = useMutation({ const createMutation = useMutation({
@@ -43,11 +51,21 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
setFormData((prev) => ({ const newValue =
...prev, type === 'checkbox'
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : ? (e.target as HTMLInputElement).checked
type === 'number' ? Number(value) : value, : type === 'number'
})); ? Number(value)
: value;
setFormData((prev) => {
const updated = { ...prev, [name]: newValue };
// Keep email_address in sync with username unless explicitly changed
if (name === 'username') {
updated.email_address = value;
}
return updated;
});
}; };
const handleProviderSelect = (config: { name: string; protocol: string; host: string; port: number; use_ssl: boolean }) => { const handleProviderSelect = (config: { name: string; protocol: string; host: string; port: number; use_ssl: boolean }) => {
@@ -110,8 +128,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
setTestMessage('Connection successful!'); setTestMessage('Connection successful!');
} catch (error) { } catch (error) {
setTestStatus('error'); setTestStatus('error');
const errorMessage = error instanceof Error && 'response' in error const errorMessage = error instanceof Error && 'response' in error
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail ? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
: null; : null;
setTestMessage(errorMessage || 'Connection failed'); setTestMessage(errorMessage || 'Connection failed');
} }
@@ -122,8 +140,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
try { try {
await createMutation.mutateAsync(formData); await createMutation.mutateAsync(formData);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error && 'response' in error const errorMessage = error instanceof Error && 'response' in error
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail ? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
: null; : null;
alert(errorMessage || 'Failed to save account'); alert(errorMessage || 'Failed to save account');
} }
@@ -131,10 +149,10 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
return ( return (
<div className="fixed inset-0 z-50 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-screen items-center justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0"> <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} /> <div className="fixed inset-0 bg-gray-500/75 transition-opacity" onClick={onClose} />
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full"> <div className="relative z-10 bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all w-full sm:my-8 sm:max-w-2xl">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="bg-white px-6 pt-6 pb-4"> <div className="bg-white px-6 pt-6 pb-4">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
@@ -270,21 +288,69 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
</div> </div>
</div> </div>
<div className="flex items-center"> <div className="flex items-center gap-6">
<input <div className="flex items-center">
type="checkbox" <input
name="use_ssl" type="checkbox"
id="use_ssl" name="use_ssl"
checked={formData.use_ssl} id="use_ssl"
onChange={handleChange} checked={formData.use_ssl}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" onChange={handleChange}
/> className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
<label htmlFor="use_ssl" className="ml-2 block text-sm text-gray-700"> />
Use SSL/TLS <label htmlFor="use_ssl" className="ml-2 block text-sm text-gray-700">
Use SSL/TLS
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
name="delete_after_forward"
id="delete_after_forward"
checked={formData.delete_after_forward}
onChange={handleChange}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="delete_after_forward" className="ml-2 block text-sm text-gray-700">
Delete after forwarding
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Forward To (destination email)
</label> </label>
<input
type="email"
name="forward_to"
value={formData.forward_to}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="you@gmail.com"
/>
<p className="mt-1 text-xs text-gray-500">
Fetched emails will be delivered to this address.
</p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Delivery Method
</label>
<select
name="delivery_method"
value={formData.delivery_method}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="gmail_api">Gmail API (recommended)</option>
<option value="smtp">SMTP</option>
</select>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Check Interval (minutes) Check Interval (minutes)
@@ -296,31 +362,25 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
onChange={handleChange} onChange={handleChange}
required required
min="1" min="1"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" max="1440"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Max Emails Per Check
</label>
<input
type="number"
name="max_emails_per_check"
value={formData.max_emails_per_check}
onChange={handleChange}
min="1"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
</div> </div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-3"> <div>
<p className="text-sm text-blue-800"> <label className="block text-sm font-medium text-gray-700 mb-1">
<strong>Delivery:</strong> Emails will be delivered to your Gmail account. Max Emails Per Check
Configure your Gmail API credentials in Settings for direct injection (recommended), </label>
or they will be forwarded via SMTP. <input
</p> type="number"
name="max_emails_per_check"
value={formData.max_emails_per_check}
onChange={handleChange}
min="1"
max="1000"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div> </div>
{testStatus !== 'idle' && ( {testStatus !== 'idle' && (
+1 -1
View File
@@ -77,7 +77,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
{/* Mobile sidebar */} {/* Mobile sidebar */}
{sidebarOpen && ( {sidebarOpen && (
<div className="fixed inset-0 z-40 lg:hidden"> <div className="fixed inset-0 z-40 lg:hidden">
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} /> <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="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"> <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">POP3 Forwarder</h1> <h1 className="text-xl font-bold text-gray-900">POP3 Forwarder</h1>
+7 -2
View File
@@ -79,7 +79,7 @@ export interface MailAccount {
export interface MailAccountCreate { export interface MailAccountCreate {
name: string; name: string;
email_address?: string; email_address: string;
protocol: string; protocol: string;
host: string; host: string;
port: number; port: number;
@@ -87,7 +87,7 @@ export interface MailAccountCreate {
use_tls?: boolean; use_tls?: boolean;
username: string; username: string;
password: string; password: string;
forward_to?: string; forward_to: string;
delivery_method?: string; delivery_method?: string;
is_enabled?: boolean; is_enabled?: boolean;
check_interval_minutes?: number; check_interval_minutes?: number;
@@ -176,6 +176,11 @@ export const userApi = {
const response = await api.get<User>("/users/me"); const response = await api.get<User>("/users/me");
return response.data; return response.data;
}, },
async updateProfile(data: { full_name?: string; email?: string }): Promise<User> {
const response = await api.put<User>("/users/me", data);
return response.data;
},
}; };
// ── Mail Accounts API ─────────────────────────────────────────────────── // ── Mail Accounts API ───────────────────────────────────────────────────