okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
8e3e173
main
1package tui 2 3import ( 4"errors" 5"math" 6"time" 7 8"albedo/cli/internal/daemon" 9"albedo/cli/internal/daemon/protocol" 10 11 tea"charm.land/bubbletea/v2" 12) 13 14// quotaNow is the clock every quota shot is drawn at, in UTC so the pictures 15// do not depend on the machine. 16var quotaNow = time .Date (2026 ,10 ,9 ,12 ,0 ,0 ,0 ,time .UTC ) 17 18func quotaReading (provider ,account ,id ,label string ,percent float64 ,window string ,seconds int64 ,resetsIn ,seenAgo time.Duration ) daemon.QuotaObservation { 19resets := quotaNow .Add (resetsIn ).Format (time .RFC3339 ) 20return daemon.QuotaObservation { 21Provider :provider ,AccountID :account ,LimitID :id ,Label :label , 22UsedPercent :& percent ,WindowLabel :& window ,WindowSeconds :& seconds , 23ResetsAt :& resets ,Status :"ok" ,Source :"poll" , 24ObservedAt :quotaNow .Add (- seenAgo ).Format (time .RFC3339 ), 25 } 26} 27 28func quotaFixture () []daemon.QuotaObservation { 29plan := "Max 5x" 30five := quotaReading ("anthropic" ,"dawn@example.com" ,"five_hour" ,"5 hour" ,62 ,"5 hour" ,5 * 3600 ,2 * time .Hour + 14 * time .Minute ,4 * time .Minute ) 31five .Plan = & plan 32week := quotaReading ("anthropic" ,"dawn@example.com" ,"seven_day" ,"7 day" ,23 ,"7 day" ,7 * 24 * 3600 ,4 * 24 * time .Hour + 3 * time .Hour ,4 * time .Minute ) 33week .Plan = & plan 34hyper := quotaReading ("hyper" ,"charm-hyper" ,"primary" ,"Primary" ,85 ,"1 hour" ,3600 ,21 * time .Minute ,time .Minute ) 35codex := quotaReading ("openai-codex" ,"dawn-chatgpt" ,"primary" ,"5 hour" ,97 ,"5 hour" ,5 * 3600 ,38 * time .Minute ,2 * time .Minute ) 36stale := quotaReading ("google-antigravity" ,"dawn@gmail.com" ,"gemini" ,"Gemini" ,41 ,"1 day" ,24 * 3600 ,20 * time .Hour ,3 * time .Hour ) 37broken := daemon.QuotaObservation { 38Provider :"deepseek" ,AccountID :"deepseek" ,LimitID :"balance" ,Label :"Balance" , 39Status :"error" ,Source :"poll" ,ObservedAt :quotaNow .Add (- 6 * time .Minute ).Format (time .RFC3339 ), 40Error :& protocol.SafeReason {Code :"quota_failed" ,Detail :"the provider answered 401: the API key was rejected" }, 41 } 42return []daemon.QuotaObservation {five ,week ,hyper ,codex ,stale ,broken } 43} 44 45// quotaTrailFixture is a limit's readings every ten minutes over its trail: 46// in each window the percent climbs from nothing toward that window's peak 47// and falls back at the reset. The newest window ends at the reading itself. 48func quotaTrailFixture (r daemon.QuotaObservation ) []quotaPoint { 49window := float64 (* r .WindowSeconds )* float64 (time .Second ) 50resets ,_ := resetOf (r ) 51seen ,_ := parseStamp (r .ObservedAt ) 52// windowsBack counts whole windows between at and the one the reset closes. 53phase := func (at time.Time ) (back int ,progress float64 ) { 54x := float64 (resets .Sub (at ))/ window 55return int (math .Floor (x )),1 - (x - math .Floor (x )) 56 } 57_ ,now := phase (seen ) 58peaks := []float64 {0 ,71 ,44 ,88 ,52 ,36 ,64 ,79 } 59var points []quotaPoint 60for at := seen .Add (- quotaTrailSpan (r ));! at .After (seen );at = at .Add (10 * time .Minute ) { 61back ,progress := phase (at ) 62peak := peaks [back % len (peaks )] 63if back == 0 { 64peak = * r .UsedPercent / max (now ,0.05 ) 65 } 66points = append (points ,quotaPoint {At :at ,Percent :min (100 ,peak * progress )}) 67 } 68return points 69} 70 71func quotaModel (width ,height int )QuotaModel { 72m := NewQuotaModel (nil ) 73m .SetSize (width ,height ) 74m ,_ = m .Update (quotaLoadedMsg {Gen :m .Generation ,At :quotaNow ,Readings :quotaFixture ()}) 75return m 76} 77 78// quotaWithTrails is a screen whose limits have all had their history read. 79func quotaWithTrails (width ,height int )QuotaModel { 80m := quotaModel (width ,height ) 81for _ ,r := range m .readings { 82if r .UsedPercent != nil { 83m ,_ = m .Update (quotaHistoryMsg {Gen :m .Generation ,Key :quotaKey (r ),Points :quotaTrailFixture (r )}) 84 } 85 } 86return m 87} 88 89func quotaShot (model func (width ,height int )QuotaModel ,keys ... tea.Msg )func (width ,height int )string { 90return func (width ,height int )string { 91m := model (width ,height ) 92for _ ,key := range keys { 93m ,_ = m .Update (key ) 94 } 95return m .View () 96 } 97} 98 99func init () { 100down := tea.KeyPressMsg {Code :tea .KeyDown } 101registerShots ("quota" , 102shotState {"list" ,quotaShot (quotaWithTrails )}, 103shotState {"warm" ,quotaShot (quotaWithTrails ,down ,down ,down ,down )}, 104shotState {"hot" ,quotaShot (quotaWithTrails ,down ,down ,down ,down ,down )}, 105shotState {"stale" ,quotaShot (quotaWithTrails ,down ,down ,down )}, 106shotState {"failed" ,quotaShot (quotaWithTrails ,down ,down )}, 107shotState {"history-loading" ,quotaShot (quotaModel )}, 108shotState {"filtering" ,quotaShot (quotaWithTrails ,shotTyped ("hyper" )... )}, 109shotState {"no-match" ,quotaShot (quotaWithTrails ,shotTyped ("zzz" )... )}, 110shotState {"loading" ,func (width ,height int )string { 111m := NewQuotaModel (nil ) 112m .SetSize (width ,height ) 113return m .View () 114 }}, 115shotState {"empty" ,func (width ,height int )string { 116m := NewQuotaModel (nil ) 117m .SetSize (width ,height ) 118m ,_ = m .Update (quotaLoadedMsg {Gen :m .Generation ,At :quotaNow }) 119return m .View () 120 }}, 121shotState {"load-failed" ,func (width ,height int )string { 122m := NewQuotaModel (nil ) 123m .SetSize (width ,height ) 124m ,_ = m .Update (quotaLoadedMsg {Gen :m .Generation ,Err :errors .New ("daemon connection unavailable" )}) 125return m .View () 126 }}, 127 ) 128}