fix: wizard grey screen, missing form fields, and implement settings page
- Fix Tailwind v4 modal overlay: bg-opacity-75 removed, use bg-gray-500/75 - Restructure AddMailAccountModal with flexbox + relative z-10 - Add missing required fields to form: forward_to, delivery_method, delete_after_forward; auto-sync email_address from username - Fix DashboardLayout mobile overlay opacity (same Tailwind v4 issue) - Make email_address/forward_to required in MailAccountCreate interface - Add userApi.updateProfile() method - Implement Settings page: profile, account info, security sections - Update CHANGELOG.md and docs/TODO.md Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/d3a9e3bc-5637-45c5-b674-8ef436953614
This commit is contained in:
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- 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 `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).
|
||||
@@ -28,6 +31,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__`)
|
||||
|
||||
### 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.
|
||||
- **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)
|
||||
|
||||
+3
-2
@@ -209,9 +209,10 @@ because the API client layer is missing.
|
||||
- [x] Dashboard with stats cards and processing runs table
|
||||
- [x] Mail accounts list with CRUD operations
|
||||
- [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] `AuthGuard` for protected routes
|
||||
- [x] Fix wizard grey screen (Tailwind v4 `bg-opacity` → `/75` syntax, modal restructure)
|
||||
|
||||
### Not Started 📋
|
||||
- [ ] 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 |
|
||||
| Observability | 10% | 🔴 Needs Work |
|
||||
| Backend Features | 85% | 🟢 Near Complete |
|
||||
| Frontend | 50% | 🟡 In Progress |
|
||||
| Frontend | 65% | 🟢 Near Complete |
|
||||
|
||||
**Overall Repository Readiness**: 55% ⚠️
|
||||
|
||||
|
||||
@@ -1,19 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthGuard } from '@/components/AuthGuard';
|
||||
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() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
<SettingsContent />
|
||||
</DashboardLayout>
|
||||
</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,
|
||||
});
|
||||
|
||||
// Sync form when server data arrives
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
setProfileForm({
|
||||
full_name: currentUser.full_name || '',
|
||||
email: currentUser.email || '',
|
||||
});
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { mailAccountsApi, MailAccount, MailAccountCreate } from '@/lib/api';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { X, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { ProviderWizard } from './ProviderWizard';
|
||||
|
||||
@@ -15,6 +16,7 @@ type WizardStep = 'provider' | 'form';
|
||||
|
||||
export function AddMailAccountModal({ account, onClose }: AddMailAccountModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuthStore();
|
||||
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
|
||||
const [testMessage, setTestMessage] = useState('');
|
||||
const [autoDetecting, setAutoDetecting] = useState(false);
|
||||
@@ -22,14 +24,20 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
const [formData, setFormData] = useState<MailAccountCreate>({
|
||||
name: account?.name || '',
|
||||
protocol: account?.protocol || 'pop3',
|
||||
email_address: account?.email_address || '',
|
||||
protocol: account?.protocol || 'pop3_ssl',
|
||||
host: account?.host || '',
|
||||
port: account?.port || 995,
|
||||
username: '',
|
||||
password: '',
|
||||
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,
|
||||
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({
|
||||
@@ -43,11 +51,21 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked :
|
||||
type === 'number' ? Number(value) : value,
|
||||
}));
|
||||
const newValue =
|
||||
type === 'checkbox'
|
||||
? (e.target as HTMLInputElement).checked
|
||||
: 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 }) => {
|
||||
@@ -110,8 +128,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
setTestMessage('Connection successful!');
|
||||
} catch (error) {
|
||||
setTestStatus('error');
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
: null;
|
||||
setTestMessage(errorMessage || 'Connection failed');
|
||||
}
|
||||
@@ -122,8 +140,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
try {
|
||||
await createMutation.mutateAsync(formData);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
: null;
|
||||
alert(errorMessage || 'Failed to save account');
|
||||
}
|
||||
@@ -131,10 +149,10 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
return (
|
||||
<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="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} />
|
||||
<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/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}>
|
||||
<div className="bg-white px-6 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -270,21 +288,69 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="use_ssl"
|
||||
id="use_ssl"
|
||||
checked={formData.use_ssl}
|
||||
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
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="use_ssl"
|
||||
id="use_ssl"
|
||||
checked={formData.use_ssl}
|
||||
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>
|
||||
</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>
|
||||
<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 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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Check Interval (minutes)
|
||||
@@ -296,31 +362,25 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
onChange={handleChange}
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
max="1440"
|
||||
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 className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Delivery:</strong> Emails will be delivered to your Gmail account.
|
||||
Configure your Gmail API credentials in Settings for direct injection (recommended),
|
||||
or they will be forwarded via SMTP.
|
||||
</p>
|
||||
<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"
|
||||
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>
|
||||
|
||||
{testStatus !== 'idle' && (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{/* Mobile sidebar */}
|
||||
{sidebarOpen && (
|
||||
<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="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>
|
||||
|
||||
@@ -79,7 +79,7 @@ export interface MailAccount {
|
||||
|
||||
export interface MailAccountCreate {
|
||||
name: string;
|
||||
email_address?: string;
|
||||
email_address: string;
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -87,7 +87,7 @@ export interface MailAccountCreate {
|
||||
use_tls?: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
forward_to?: string;
|
||||
forward_to: string;
|
||||
delivery_method?: string;
|
||||
is_enabled?: boolean;
|
||||
check_interval_minutes?: number;
|
||||
@@ -176,6 +176,11 @@ export const userApi = {
|
||||
const response = await api.get<User>("/users/me");
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user