@@ -12,6 +12,8 @@ import {
1212 Plus ,
1313 BookOpen ,
1414 Github ,
15+ Receipt ,
16+ ExternalLink ,
1517} from "lucide-react" ;
1618import { Button } from "@/components/ui/button" ;
1719import { Badge } from "@/components/ui/badge" ;
@@ -49,10 +51,19 @@ import { useUserStore } from "@/stores/user";
4951import { useTopNavStore } from "@/stores/top-nav" ;
5052import { usePlanStore , Price , Product , getPlanTypeDisplayName , isPaidPlan } from "@/stores/plan" ;
5153import { User } from "@supabase/supabase-js" ;
52- import { cn } from "@/lib/utils" ;
54+ import { cn , formatBytes } from "@/lib/utils" ;
5355import { encodeId } from "@/lib/id-codec" ;
56+ import {
57+ HoverCard ,
58+ HoverCardContent ,
59+ HoverCardTrigger ,
60+ } from "@/components/ui/hover-card" ;
5461import { SidebarTriggerWrapper } from "@/components/sidebar-trigger-wrapper" ;
5562import { AlertBanner } from "@/components/alert-banner" ;
63+ import {
64+ getAllOrganizationsUsage ,
65+ type OrganizationUsageSummary ,
66+ } from "@/lib/supabase/operations/organizations" ;
5667
5768const EXTERNAL_LINKS = [
5869 {
@@ -282,6 +293,162 @@ function ProjectSelector({
282293 ) ;
283294}
284295
296+ // Usage Indicator Component
297+ function UsageIndicator ( { className } : { className ?: string } ) {
298+ const [ usageData , setUsageData ] = React . useState < OrganizationUsageSummary [ ] > (
299+ [ ]
300+ ) ;
301+ const [ loading , setLoading ] = React . useState ( false ) ;
302+ const [ fetched , setFetched ] = React . useState ( false ) ;
303+ const router = useRouter ( ) ;
304+
305+ const fetchUsage = React . useCallback ( async ( ) => {
306+ if ( fetched ) return ;
307+ setLoading ( true ) ;
308+ try {
309+ const data = await getAllOrganizationsUsage ( ) ;
310+ setUsageData ( data ) ;
311+ setFetched ( true ) ;
312+ } catch {
313+ // silently fail
314+ } finally {
315+ setLoading ( false ) ;
316+ }
317+ } , [ fetched ] ) ;
318+
319+ // Auto-fetch on mount to show warning dot
320+ React . useEffect ( ( ) => {
321+ fetchUsage ( ) ;
322+ } , [ fetchUsage ] ) ;
323+
324+ // Check if any org has critical usage (>=90%)
325+ const hasWarning = React . useMemo ( ( ) => {
326+ return usageData . some ( ( org ) => {
327+ const metrics = [
328+ { current : org . usage . current_task , max : org . limits . max_task } ,
329+ { current : org . usage . current_storage , max : org . limits . max_storage } ,
330+ ] ;
331+ return metrics . some (
332+ ( m ) => m . max > 0 && ( m . current / m . max ) * 100 >= 90
333+ ) ;
334+ } ) ;
335+ } , [ usageData ] ) ;
336+
337+ const getBarColor = ( percentage : number ) => {
338+ if ( percentage >= 90 ) return "bg-red-500" ;
339+ if ( percentage >= 70 ) return "bg-amber-500" ;
340+ return "bg-primary" ;
341+ } ;
342+
343+ return (
344+ < HoverCard openDelay = { 200 } closeDelay = { 150 } >
345+ < HoverCardTrigger asChild >
346+ < Button
347+ variant = "ghost"
348+ size = "icon"
349+ className = { cn ( "rounded-full h-8 w-8 relative border border-border" , className ) }
350+ >
351+ < Receipt className = "h-4 w-4" />
352+ { hasWarning && (
353+ < span className = "absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-red-500 border-2 border-background" />
354+ ) }
355+ </ Button >
356+ </ HoverCardTrigger >
357+ < HoverCardContent className = "w-[320px] p-0" align = "end" >
358+ < div className = "max-h-[320px] overflow-y-auto" >
359+ { loading && ! fetched ? (
360+ < div className = "px-3 py-4 text-center text-sm text-muted-foreground" >
361+ Loading...
362+ </ div >
363+ ) : usageData . length === 0 ? (
364+ < div className = "px-3 py-4 text-center text-sm text-muted-foreground" >
365+ No organizations
366+ </ div >
367+ ) : (
368+ usageData . map ( ( org ) => {
369+ const taskPct =
370+ org . limits . max_task > 0
371+ ? Math . min (
372+ ( org . usage . current_task / org . limits . max_task ) * 100 ,
373+ 100
374+ )
375+ : 0 ;
376+ const storagePct =
377+ org . limits . max_storage > 0
378+ ? Math . min (
379+ ( org . usage . current_storage / org . limits . max_storage ) *
380+ 100 ,
381+ 100
382+ )
383+ : 0 ;
384+ const maxPct = Math . max ( taskPct , storagePct ) ;
385+ const encodedId = encodeId ( org . orgId ) ;
386+
387+ return (
388+ < button
389+ key = { org . orgId }
390+ className = "w-full px-3 py-2.5 text-left hover:bg-muted/50 transition-colors border-b last:border-b-0 cursor-pointer"
391+ onClick = { ( ) => {
392+ router . push ( `/org/${ encodedId } /billing` ) ;
393+ } }
394+ >
395+ < div className = "flex items-center justify-between mb-1.5" >
396+ < div className = "flex items-center gap-1.5 min-w-0" >
397+ < span className = "text-sm font-medium truncate" >
398+ { org . orgName }
399+ </ span >
400+ { maxPct >= 90 && (
401+ < span className = "h-1.5 w-1.5 rounded-full bg-red-500 shrink-0" />
402+ ) }
403+ </ div >
404+ < ExternalLink className = "h-3 w-3 text-muted-foreground shrink-0" />
405+ </ div >
406+ { /* Agent Tasks */ }
407+ < div className = "space-y-1" >
408+ < div className = "flex items-center justify-between text-xs text-muted-foreground" >
409+ < span > Tasks</ span >
410+ < span className = "tabular-nums" >
411+ { org . usage . current_task . toLocaleString ( ) } /{ " " }
412+ { org . limits . max_task > 0
413+ ? org . limits . max_task . toLocaleString ( )
414+ : "∞" }
415+ </ span >
416+ </ div >
417+ < div className = "h-1.5 w-full overflow-hidden rounded-full bg-muted" >
418+ < div
419+ className = { `h-full rounded-full transition-all ${ getBarColor ( taskPct ) } ` }
420+ style = { { width : `${ taskPct } %` } }
421+ />
422+ </ div >
423+ </ div >
424+ { /* Storage */ }
425+ < div className = "space-y-1 mt-1.5" >
426+ < div className = "flex items-center justify-between text-xs text-muted-foreground" >
427+ < span > Storage</ span >
428+ < span className = "tabular-nums" >
429+ { formatBytes ( org . usage . current_storage ) } /{ " " }
430+ { org . limits . max_storage > 0
431+ ? formatBytes ( org . limits . max_storage )
432+ : "∞" }
433+ </ span >
434+ </ div >
435+ < div className = "h-1.5 w-full overflow-hidden rounded-full bg-muted" >
436+ < div
437+ className = { `h-full rounded-full transition-all ${ getBarColor ( storagePct ) } ` }
438+ style = { { width : `${ storagePct } %` } }
439+ />
440+ </ div >
441+ </ div >
442+ </ button >
443+ ) ;
444+ } )
445+ ) }
446+ </ div >
447+ </ HoverCardContent >
448+ </ HoverCard >
449+ ) ;
450+ }
451+
285452export function TopNav ( { user, prices = [ ] , products = [ ] } : TopNavProps ) {
286453 // Get data from stores
287454 const { setUser } = useUserStore ( ) ;
@@ -366,6 +533,8 @@ export function TopNav({ user, prices = [], products = [] }: TopNavProps) {
366533 </ Link >
367534 < div className = "flex gap-2 min-w-0 ml-3" >
368535 < AlertBanner variant = "mobile" />
536+ { /* Usage Indicator - shown on mobile */ }
537+ < UsageIndicator className = "md:hidden" />
369538 { /* External links - shown on mobile */ }
370539 { EXTERNAL_LINKS . map ( ( link ) => {
371540 const Icon = link . icon ;
@@ -504,6 +673,9 @@ export function TopNav({ user, prices = [], products = [] }: TopNavProps) {
504673 </ a >
505674 </ Button >
506675
676+ { /* Usage Indicator - desktop only */ }
677+ < UsageIndicator className = "hidden md:inline-flex" />
678+
507679 { /* External links - shown on medium screens and above (hidden on small screens where they appear in Mobile Top Layer) */ }
508680 { EXTERNAL_LINKS . map ( ( link ) => {
509681 const Icon = link . icon ;
0 commit comments