diff --git a/.impeccable.md b/.impeccable.md index 5b5813d6..b3ce9531 100644 --- a/.impeccable.md +++ b/.impeccable.md @@ -34,7 +34,7 @@ **Theme:** Dark mode primary (charcoal palette). Light mode planned but not yet implemented. -**Accent:** Ember orange (#f97316) — conveys urgency fitting a troubleshooting context. Used sparingly (max 5% of UI). Warning uses yellow (#eab308), not amber, to stay distinct. +**Accent:** Electric blue (#60a5fa dark / #2563eb light) — conveys trust, precision, and reliability fitting a troubleshooting tool MSP engineers depend on during outages. Used sparingly (max 5% of UI). Warning uses amber (#fbbf24), info uses cyan (#67e8f9). **Hard rules:** No glassmorphism, no gradient surfaces, no ambient orbs, no backdrop blur, no decorative shadows at rest. Elevation = lighter surface + border, not shadow. diff --git a/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx b/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx index 41093810..3f6e8b08 100644 --- a/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx +++ b/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx @@ -4,16 +4,7 @@ import { Clock, ArrowRight, Route, MessageCircle } from 'lucide-react' import { aiSessionsApi } from '@/api/aiSessions' import type { AISessionSummary } from '@/types/ai-session' import { cn } from '@/lib/utils' - -function timeAgo(dateStr: string): string { - const diffMs = Date.now() - new Date(dateStr).getTime() - const minutes = Math.floor(diffMs / 60000) - if (minutes < 1) return 'just now' - if (minutes < 60) return `${minutes}m ago` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h ago` - return `${Math.floor(hours / 24)}d ago` -} +import { timeAgo } from '@/lib/timeAgo' export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: boolean }) { const [sessions, setSessions] = useState([]) @@ -68,7 +59,7 @@ export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: b )} { sidebarApi.getStats() .then((stats) => setFlowCount(stats.tree_counts.total)) .catch(() => {}) + .finally(() => setLoading(false)) }, []) const items = [ @@ -33,23 +36,35 @@ export function KnowledgeBaseCards() { Browse -
- {items.map((item) => ( +
+ {loading ? Array.from({ length: 3 }).map((_, i) => ( +
+ + + +
+ )) : items.map((item, i) => ( - ))} + )))}
) diff --git a/frontend/src/components/dashboard/PendingEscalations.tsx b/frontend/src/components/dashboard/PendingEscalations.tsx index 2d36a561..7db7955a 100644 --- a/frontend/src/components/dashboard/PendingEscalations.tsx +++ b/frontend/src/components/dashboard/PendingEscalations.tsx @@ -3,16 +3,7 @@ import { Link, useNavigate } from 'react-router-dom' import { AlertTriangle } from 'lucide-react' import { aiSessionsApi } from '@/api/aiSessions' import type { AISessionSummary } from '@/types/ai-session' - -function timeAgo(dateStr: string): string { - const diffMs = Date.now() - new Date(dateStr).getTime() - const minutes = Math.floor(diffMs / 60000) - if (minutes < 1) return 'just now' - if (minutes < 60) return `${minutes}m ago` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h ago` - return `${Math.floor(hours / 24)}d ago` -} +import { timeAgo } from '@/lib/timeAgo' export function PendingEscalations() { const [escalations, setEscalations] = useState([]) diff --git a/frontend/src/components/dashboard/PerformanceCards.tsx b/frontend/src/components/dashboard/PerformanceCards.tsx index 6ae67aa6..c8dbb701 100644 --- a/frontend/src/components/dashboard/PerformanceCards.tsx +++ b/frontend/src/components/dashboard/PerformanceCards.tsx @@ -4,6 +4,7 @@ import { CheckCircle, Clock, TrendingUp, Timer } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { sidebarApi } from '@/api' import { cn } from '@/lib/utils' +import { Skeleton } from '@/components/ui/Skeleton' interface StatCard { label: string @@ -19,6 +20,8 @@ export function PerformanceCards() { const [resolved, setResolved] = useState(0) const [active, setActive] = useState(0) const [totalMinutes, setTotalMinutes] = useState(0) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) useEffect(() => { sidebarApi.getStats() @@ -27,9 +30,31 @@ export function PerformanceCards() { setActive(stats.active_count) setTotalMinutes(stats.total_session_minutes_today) }) - .catch(() => {}) + .catch(() => setError(true)) + .finally(() => setLoading(false)) }, []) + if (loading) { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ ) + } + + if (error) { + return ( +
+

Unable to load performance data

+
+ ) + } + const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0 const cards: StatCard[] = [ @@ -70,14 +95,13 @@ export function PerformanceCards() { -
- {items.map((item) => ( +
+ {loading ? Array.from({ length: 3 }).map((_, i) => ( +
+ + + +
+ )) : items.map((item) => ( - ))} + )))}
) diff --git a/frontend/src/components/layout/NotificationsPanel.tsx b/frontend/src/components/layout/NotificationsPanel.tsx index 8f634173..a261a068 100644 --- a/frontend/src/components/layout/NotificationsPanel.tsx +++ b/frontend/src/components/layout/NotificationsPanel.tsx @@ -10,14 +10,7 @@ import { } from 'lucide-react' import { notificationsApi } from '@/api/notifications' import type { AppNotification } from '@/types/notification' - -function timeAgo(dateStr: string): string { - const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000) - if (diff < 60) return 'just now' - if (diff < 3600) return `${Math.floor(diff / 60)}m ago` - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` - return `${Math.floor(diff / 86400)}d ago` -} +import { timeAgo } from '@/lib/timeAgo' function EventIcon({ event }: { event: string }) { switch (event) { diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx index 9890d317..c3c66b15 100644 --- a/frontend/src/components/ui/Skeleton.tsx +++ b/frontend/src/components/ui/Skeleton.tsx @@ -6,7 +6,7 @@ export function Skeleton({ className, ...props }: SkeletonProps) { return (
- {/* Hero: Greeting + Stat Strip */} -
-
-

- {dayOfWeek}, {formattedDate} -

-

- Good {greeting},
- {firstName}. -

-
- + {/* Hero: Greeting */} +
+

+ {dayOfWeek}, {formattedDate} +

+

+ Good {greeting}, {firstName}. +

{/* Chat-style input */}