diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e00297..52739a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/TODO.md b/docs/TODO.md index 96418b7..2b82d60 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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% ⚠️ diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 67d5f05..31d2454 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -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 ( -
-

Settings

-
-

Settings page coming soon...

-
-
+
); } + +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) => { + 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 ( +
+

Settings

+ + {/* Profile Section */} +
+
+ +

Profile

+
+
+
+ + +
+ +
+ + +
+ +
+ + {profileSaved && ( + + + Saved successfully + + )} +
+
+
+ + {/* Account Information */} +
+
+ +

Account Information

+
+
+
+ Subscription tier: + + {displayUser?.subscription_tier ?? 'β€”'} + +
+
+ Subscription status: + + {displayUser?.subscription_status ?? 'β€”'} + +
+
+ Member since: + + {displayUser?.created_at + ? new Date(displayUser.created_at).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : 'β€”'} + +
+ {displayUser?.oauth_provider && ( +
+ Linked account: + + {displayUser.oauth_provider} + +
+ )} + {displayUser?.last_login_at && ( +
+ Last login: + + {new Date(displayUser.last_login_at).toLocaleString()} + +
+ )} +
+
+ + {/* Security Section */} +
+
+ +

Security

+
+
+

+ Password change and two-factor authentication settings are coming soon. +

+ {displayUser?.oauth_provider === 'google' && ( +

+ Your account is authenticated via Google OAuth β€” password management is handled by Google. +

+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/AddMailAccountModal.tsx b/frontend/src/components/AddMailAccountModal.tsx index d73054f..75c2c72 100644 --- a/frontend/src/components/AddMailAccountModal.tsx +++ b/frontend/src/components/AddMailAccountModal.tsx @@ -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({ 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) => { 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 (
-
-
+
+
-
+
@@ -270,21 +288,69 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
-
- -