github.com/wtfutil/wtf@v0.43.0/modules/updown/widget.go (about) 1 package updown 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "time" 8 9 "github.com/rivo/tview" 10 "github.com/wtfutil/wtf/utils" 11 "github.com/wtfutil/wtf/view" 12 ) 13 14 const ( 15 userAgent = "WTFUtil" 16 17 apiURLBase = "https://updown.io" 18 ) 19 20 type Widget struct { 21 view.ScrollableWidget 22 checks []Check 23 settings *Settings 24 tokenSet map[string]struct{} 25 err error 26 } 27 28 // Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L17 29 type Check struct { 30 Token string `json:"token,omitempty"` 31 URL string `json:"url,omitempty"` 32 Alias string `json:"alias,omitempty"` 33 LastStatus int `json:"last_status,omitempty"` 34 Uptime float64 `json:"uptime,omitempty"` 35 Down bool `json:"down"` 36 DownSince string `json:"down_since,omitempty"` 37 Error string `json:"error,omitempty"` 38 Period int `json:"period,omitempty"` 39 Apdex float64 `json:"apdex_t,omitempty"` 40 Enabled bool `json:"enabled"` 41 Published bool `json:"published"` 42 LastCheckAt time.Time `json:"last_check_at,omitempty"` 43 NextCheckAt time.Time `json:"next_check_at,omitempty"` 44 FaviconURL string `json:"favicon_url,omitempty"` 45 SSL SSL `json:"ssl,omitempty"` 46 StringMatch string `json:"string_match,omitempty"` 47 MuteUntil string `json:"mute_until,omitempty"` 48 DisabledLocations []string `json:"disabled_locations,omitempty"` 49 CustomHeaders map[string]string `json:"custom_headers,omitempty"` 50 } 51 52 // Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L10 53 type SSL struct { 54 TestedAt string `json:"tested_at,omitempty"` 55 Valid bool `json:"valid,omitempty"` 56 Error string `json:"error,omitempty"` 57 } 58 59 func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget { 60 widget := &Widget{ 61 ScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common), 62 settings: settings, 63 tokenSet: make(map[string]struct{}), 64 } 65 66 for _, t := range settings.tokens { 67 widget.tokenSet[t] = struct{}{} 68 } 69 70 widget.SetRenderFunction(widget.Render) 71 widget.initializeKeyboardControls() 72 73 return widget 74 } 75 76 func (widget *Widget) Refresh() { 77 if widget.Disabled() { 78 return 79 } 80 81 checks, err := widget.getExistingChecks() 82 widget.checks = checks 83 widget.err = err 84 widget.SetItemCount(len(checks)) 85 widget.Render() 86 } 87 88 // Render sets up the widget data for redrawing to the screen 89 func (widget *Widget) Render() { 90 widget.Redraw(widget.content) 91 } 92 93 func (widget *Widget) content() (string, string, bool) { 94 numUp := 0 95 for _, check := range widget.checks { 96 if !check.Down { 97 numUp++ 98 } 99 } 100 101 title := fmt.Sprintf("Updown (%d/%d)", numUp, len(widget.checks)) 102 103 if widget.err != nil { 104 return title, widget.err.Error(), true 105 } 106 107 if widget.checks == nil { 108 return title, "No checks to display", false 109 } 110 111 str := widget.contentFrom(widget.checks) 112 113 return title, str, false 114 } 115 116 func (widget *Widget) contentFrom(checks []Check) string { 117 var str string 118 119 for _, check := range checks { 120 prefix := "" 121 122 if !check.Enabled { 123 prefix += "[yellow] ~ " 124 } else if check.Down { 125 prefix += "[red] - " 126 } else { 127 prefix += "[green] + " 128 } 129 130 str += fmt.Sprintf(`%s%s [gray](%0.2f|%s)[white]%s`, 131 prefix, 132 check.Alias, 133 check.Uptime, 134 timeSincePing(check.LastCheckAt), 135 "\n", 136 ) 137 } 138 139 return str 140 } 141 142 func timeSincePing(ts time.Time) string { 143 dur := time.Since(ts) 144 return dur.Truncate(time.Second).String() 145 } 146 147 func makeURL(baseurl string, path string) (string, error) { 148 u, err := url.Parse(baseurl) 149 if err != nil { 150 return "", err 151 } 152 u.Path = path 153 return u.String(), nil 154 } 155 156 func filterChecks(checks []Check, tokenSet map[string]struct{}) []Check { 157 j := 0 158 for i := 0; i < len(checks); i++ { 159 if _, ok := tokenSet[checks[i].Token]; ok { 160 checks[j] = checks[i] 161 j++ 162 } 163 } 164 return checks[:j] 165 } 166 167 func (widget *Widget) getExistingChecks() ([]Check, error) { 168 // See: https://updown.io/api#rest 169 u, err := makeURL(apiURLBase, "/api/checks") 170 if err != nil { 171 return nil, err 172 } 173 req, err := http.NewRequest("GET", u, http.NoBody) 174 if err != nil { 175 return nil, err 176 } 177 req.Header.Set("User-Agent", userAgent) 178 req.Header.Set("X-API-KEY", widget.settings.apiKey) 179 resp, err := http.DefaultClient.Do(req) 180 181 if err != nil { 182 return nil, err 183 } 184 185 if resp.StatusCode != 200 { 186 return nil, fmt.Errorf(resp.Status) 187 } 188 189 defer func() { _ = resp.Body.Close() }() 190 191 var checks []Check 192 err = utils.ParseJSON(&checks, resp.Body) 193 if err != nil { 194 return nil, err 195 } 196 197 if len(widget.tokenSet) > 0 { 198 checks = filterChecks(checks, widget.tokenSet) 199 } 200 201 return checks, nil 202 }