diff --git a/backend/alembic/versions/003_add_preferred_name.py b/backend/alembic/versions/003_add_preferred_name.py new file mode 100644 index 0000000..06fe739 --- /dev/null +++ b/backend/alembic/versions/003_add_preferred_name.py @@ -0,0 +1,26 @@ +"""Add preferred_name to settings + +Revision ID: 003 +Revises: 002 +Create Date: 2026-02-20 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '003' +down_revision: Union[str, None] = '002' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('settings', sa.Column('preferred_name', sa.String(100), nullable=True)) + + +def downgrade() -> None: + op.drop_column('settings', 'preferred_name') diff --git a/backend/app/models/settings.py b/backend/app/models/settings.py index 4a2c605..2fd432e 100644 --- a/backend/app/models/settings.py +++ b/backend/app/models/settings.py @@ -11,5 +11,6 @@ class Settings(Base): pin_hash: Mapped[str] = mapped_column(String(255), nullable=False) accent_color: Mapped[str] = mapped_column(String(20), default="cyan") upcoming_days: Mapped[int] = mapped_column(Integer, default=7) + preferred_name: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None) created_at: Mapped[datetime] = mapped_column(default=func.now()) updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now()) diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index a94fa8b..5556c48 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -25,12 +25,14 @@ class SettingsCreate(BaseModel): class SettingsUpdate(BaseModel): accent_color: Optional[AccentColor] = None upcoming_days: int | None = None + preferred_name: str | None = None class SettingsResponse(BaseModel): id: int accent_color: str upcoming_days: int + preferred_name: str | None = None created_at: datetime updated_at: datetime diff --git a/frontend/src/components/dashboard/DashboardPage.tsx b/frontend/src/components/dashboard/DashboardPage.tsx index 16ac44d..5567f13 100644 --- a/frontend/src/components/dashboard/DashboardPage.tsx +++ b/frontend/src/components/dashboard/DashboardPage.tsx @@ -9,16 +9,18 @@ import TodoWidget from './TodoWidget'; import CalendarWidget from './CalendarWidget'; import UpcomingWidget from './UpcomingWidget'; import WeekTimeline from './WeekTimeline'; +import DayBriefing from './DayBriefing'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DashboardSkeleton } from '@/components/ui/skeleton'; -function getGreeting(): string { +function getGreeting(name?: string): string { const hour = new Date().getHours(); - if (hour < 5) return 'Good night.'; - if (hour < 12) return 'Good morning.'; - if (hour < 17) return 'Good afternoon.'; - if (hour < 21) return 'Good evening.'; - return 'Good night.'; + const suffix = name ? `, ${name}.` : '.'; + if (hour < 5) return `Good night${suffix}`; + if (hour < 12) return `Good morning${suffix}`; + if (hour < 17) return `Good afternoon${suffix}`; + if (hour < 21) return `Good evening${suffix}`; + return `Good night${suffix}`; } export default function DashboardPage() { @@ -72,7 +74,7 @@ export default function DashboardPage() { {/* Header — greeting + date */}

- {getGreeting()} + {getGreeting(settings?.preferred_name || undefined)}

{format(new Date(), 'EEEE, MMMM d, yyyy')} @@ -88,6 +90,11 @@ export default function DashboardPage() {

)} + {/* Smart Briefing */} + {upcomingData && ( + + )} + {/* Stats Row */}
{ + const now = new Date(); + const hour = now.getHours(); + const today = startOfDay(now); + const tomorrow = addDays(today, 1); + + const todayItems = upcomingItems.filter((item) => { + const d = item.datetime ? new Date(item.datetime) : new Date(item.date); + return isSameDay(startOfDay(d), today); + }); + + const tomorrowItems = upcomingItems.filter((item) => { + const d = item.datetime ? new Date(item.datetime) : new Date(item.date); + return isSameDay(startOfDay(d), tomorrow); + }); + + const todayEvents = dashboardData.todays_events; + const activeReminders = dashboardData.active_reminders; + const todayTodos = dashboardData.upcoming_todos.filter((t) => { + if (!t.due_date) return false; + return isSameDay(startOfDay(new Date(t.due_date)), today); + }); + + const parts: string[] = []; + + // Night (9PM–5AM): Focus on tomorrow + if (hour >= 21 || hour < 5) { + if (tomorrowItems.length === 0) { + parts.push('Tomorrow is clear — nothing scheduled.'); + } else { + const firstEvent = tomorrowItems.find((i) => i.type === 'event' && i.datetime); + if (firstEvent) { + parts.push( + `You have ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} tomorrow, starting with ${firstEvent.title} at ${getItemTime(firstEvent)}.` + ); + } else { + parts.push( + `You have ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} lined up for tomorrow.` + ); + } + } + } + // Morning (5AM–12PM) + else if (hour < 12) { + if (todayItems.length === 0) { + parts.push('Your day is wide open — no events or tasks scheduled.'); + } else { + const eventCount = todayEvents.length; + const todoCount = todayTodos.length; + const segments: string[] = []; + if (eventCount > 0) segments.push(`${eventCount} event${eventCount > 1 ? 's' : ''}`); + if (todoCount > 0) segments.push(`${todoCount} task${todoCount > 1 ? 's' : ''} due`); + if (segments.length > 0) { + parts.push(`Today: ${segments.join(' and ')}.`); + } + const firstEvent = todayEvents.find((e) => { + const d = new Date(e.start_datetime); + return isAfter(d, now); + }); + if (firstEvent) { + parts.push(`Up next is ${firstEvent.title} at ${format(new Date(firstEvent.start_datetime), 'h:mm a')}.`); + } + } + } + // Afternoon (12PM–5PM) + else if (hour < 17) { + const remainingEvents = todayEvents.filter((e) => isAfter(new Date(e.end_datetime), now)); + const completedTodos = todayTodos.length === 0; + if (remainingEvents.length === 0 && completedTodos) { + parts.push('The rest of your afternoon is clear.'); + } else { + if (remainingEvents.length > 0) { + parts.push( + `${remainingEvents.length} event${remainingEvents.length > 1 ? 's' : ''} remaining this afternoon.` + ); + } + if (todayTodos.length > 0) { + parts.push(`${todayTodos.length} task${todayTodos.length > 1 ? 's' : ''} still due today.`); + } + } + } + // Evening (5PM–9PM) + else { + const eveningEvents = todayEvents.filter((e) => isAfter(new Date(e.end_datetime), now)); + if (eveningEvents.length === 0 && tomorrowItems.length === 0) { + parts.push('Nothing left tonight, and tomorrow is clear too.'); + } else { + if (eveningEvents.length > 0) { + parts.push(`${eveningEvents.length} event${eveningEvents.length > 1 ? 's' : ''} left this evening.`); + } + if (tomorrowItems.length > 0) { + parts.push(`Tomorrow has ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} ahead.`); + } + } + } + + // Reminder callout + if (activeReminders.length > 0) { + const nextReminder = activeReminders[0]; + const remindTime = format(new Date(nextReminder.remind_at), 'h:mm a'); + parts.push(`Don't forget: ${nextReminder.title} at ${remindTime}.`); + } + + return parts.join(' '); + }, [upcomingItems, dashboardData]); + + if (!briefing) return null; + + return ( +

+ {briefing} +

+ ); +} diff --git a/frontend/src/components/dashboard/WeekTimeline.tsx b/frontend/src/components/dashboard/WeekTimeline.tsx index f2e1252..d19f988 100644 --- a/frontend/src/components/dashboard/WeekTimeline.tsx +++ b/frontend/src/components/dashboard/WeekTimeline.tsx @@ -72,8 +72,9 @@ export default function WeekTimeline({ items }: WeekTimelineProps) { key={`${item.type}-${item.id}`} className={cn( 'w-1.5 h-1.5 rounded-full', - typeColors[item.type] || 'bg-muted-foreground' + !item.color && (typeColors[item.type] || 'bg-muted-foreground') )} + style={item.color ? { backgroundColor: item.color } : undefined} /> ))} {day.items.length > 4 && ( diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx index 65979dc..6cd8a46 100644 --- a/frontend/src/components/settings/SettingsPage.tsx +++ b/frontend/src/components/settings/SettingsPage.tsx @@ -20,12 +20,24 @@ export default function SettingsPage() { const { settings, updateSettings, changePin, isUpdating, isChangingPin } = useSettings(); const [selectedColor, setSelectedColor] = useState(settings?.accent_color || 'cyan'); const [upcomingDays, setUpcomingDays] = useState(settings?.upcoming_days || 7); + const [preferredName, setPreferredName] = useState(settings?.preferred_name || ''); const [pinForm, setPinForm] = useState({ oldPin: '', newPin: '', confirmPin: '', }); + const handleNameSave = async () => { + const trimmed = preferredName.trim(); + if (trimmed === (settings?.preferred_name || '')) return; + try { + await updateSettings({ preferred_name: trimmed || null }); + toast.success('Name updated'); + } catch (error) { + toast.error('Failed to update name'); + } + }; + const handleColorChange = async (color: string) => { setSelectedColor(color); try { @@ -73,6 +85,34 @@ export default function SettingsPage() {
+ + + Profile + Personalize how UMBRA greets you + + +
+ +
+ setPreferredName(e.target.value)} + onBlur={handleNameSave} + onKeyDown={(e) => { if (e.key === 'Enter') handleNameSave(); }} + className="max-w-xs" + maxLength={100} + /> +
+

+ Used in the dashboard greeting, e.g. "Good morning, {preferredName || 'Kyle'}." +

+
+
+
+ Appearance diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index 84ecdb4..10da235 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -14,7 +14,7 @@ export function useSettings() { }); const updateMutation = useMutation({ - mutationFn: async (updates: Partial) => { + mutationFn: async (updates: Partial & { preferred_name?: string | null }) => { const { data } = await api.put('/settings', updates); return data; }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 7ab4af4..f130874 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -2,6 +2,7 @@ export interface Settings { id: number; accent_color: string; upcoming_days: number; + preferred_name?: string; created_at: string; updated_at: string; }