Eric Bower
·
2026-01-16
access_logs.go
1package tui
2
3import (
4 "fmt"
5 "time"
6
7 "git.sr.ht/~rockorager/vaxis"
8 "git.sr.ht/~rockorager/vaxis/vxfw"
9 "git.sr.ht/~rockorager/vaxis/vxfw/list"
10 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
11 "git.sr.ht/~rockorager/vaxis/vxfw/text"
12 "github.com/picosh/pico/pkg/db"
13)
14
15type AccessLogsLoaded struct{}
16
17type AccessLogsPage struct {
18 shared *SharedModel
19
20 input *TextInput
21 list *list.Dynamic
22 filtered []int
23 logs []*db.AccessLog
24 loading bool
25 err error
26 focus string
27}
28
29func NewAccessLogsPage(shrd *SharedModel) *AccessLogsPage {
30 page := &AccessLogsPage{
31 shared: shrd,
32 input: NewTextInput("filter logs"),
33 focus: "input",
34 }
35 page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true}
36 return page
37}
38
39func (m *AccessLogsPage) Footer() []Shortcut {
40 return []Shortcut{
41 {Shortcut: "tab", Text: "focus"},
42 {Shortcut: "^r", Text: "refresh"},
43 {Shortcut: "g", Text: "top"},
44 {Shortcut: "G", Text: "bottom"},
45 }
46}
47
48func (m *AccessLogsPage) filterLogLine(match string, ll *db.AccessLog) bool {
49 if match == "" {
50 return true
51 }
52
53 serviceMatch := matched(ll.Service, match)
54 identityMatch := matched(ll.Identity, match)
55 pubkeyMatch := matched(ll.Pubkey, match)
56
57 return serviceMatch || identityMatch || pubkeyMatch
58}
59
60func (m *AccessLogsPage) filterLogs() {
61 if m.loading || len(m.logs) == 0 {
62 m.filtered = []int{}
63 return
64 }
65
66 match := m.input.GetValue()
67 filtered := []int{}
68 for idx, ll := range m.logs {
69 if m.filterLogLine(match, ll) {
70 filtered = append(filtered, idx)
71 }
72 }
73
74 m.filtered = filtered
75
76 if len(filtered) > 0 {
77 m.list.SetCursor(uint(len(filtered) - 1))
78 }
79}
80
81func (m *AccessLogsPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
82 switch msg := ev.(type) {
83 case vaxis.Key:
84 if msg.Matches(vaxis.KeyTab) {
85 return nil, nil
86 }
87 if m.focus == "input" {
88 m.filterLogs()
89 return vxfw.RedrawCmd{}, nil
90 }
91 }
92 return nil, nil
93}
94
95func (m *AccessLogsPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
96 switch msg := ev.(type) {
97 case PageIn:
98 m.loading = true
99 go m.fetchLogs()
100 m.focus = "input"
101 return m.input.FocusIn()
102 case AccessLogsLoaded:
103 return vxfw.RedrawCmd{}, nil
104 case vaxis.Key:
105 if msg.Matches(vaxis.KeyTab) {
106 if m.focus == "input" {
107 m.focus = "access logs"
108 cmd, _ := m.input.FocusOut()
109 return vxfw.BatchCmd([]vxfw.Command{
110 vxfw.FocusWidgetCmd(m.list),
111 cmd,
112 }), nil
113 }
114 m.focus = "input"
115 cmd, _ := m.input.FocusIn()
116 return vxfw.BatchCmd([]vxfw.Command{
117 cmd,
118 vxfw.RedrawCmd{},
119 }), nil
120 }
121 if msg.Matches('r', vaxis.ModCtrl) {
122 m.loading = true
123 go m.fetchLogs()
124 return vxfw.RedrawCmd{}, nil
125 }
126 if msg.Matches('g') {
127 // Go to top
128 if len(m.filtered) > 0 {
129 m.list.SetCursor(0)
130 return vxfw.RedrawCmd{}, nil
131 }
132 }
133 if msg.Matches('G') {
134 // Go to bottom
135 if len(m.filtered) > 0 {
136 m.list.SetCursor(uint(len(m.filtered) - 1))
137 return vxfw.RedrawCmd{}, nil
138 }
139 }
140 }
141 return nil, nil
142}
143
144func (m *AccessLogsPage) fetchLogs() {
145 if m.shared.User == nil {
146 m.err = fmt.Errorf("no user found")
147 m.loading = false
148 m.shared.App.PostEvent(AccessLogsLoaded{})
149 return
150 }
151
152 fromDate := time.Now().AddDate(0, 0, -30)
153 logs, err := m.shared.Dbpool.FindAccessLogs(m.shared.User.ID, &fromDate)
154
155 m.loading = false
156 if err != nil {
157 m.err = err
158 m.logs = []*db.AccessLog{}
159 } else {
160 m.err = nil
161 m.logs = logs
162 }
163
164 m.filterLogs()
165 m.shared.App.PostEvent(AccessLogsLoaded{})
166}
167
168func (m *AccessLogsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
169 w := ctx.Max.Width
170 h := ctx.Max.Height
171 root := vxfw.NewSurface(w, h, m)
172 ah := 0
173
174 logsLen := len(m.logs)
175 filteredLen := len(m.filtered)
176 err := m.err
177
178 info := text.New("Access logs in the last 30 days. Access logs show SSH connections to pico services.")
179 brd := NewBorder(info)
180 brd.Label = "desc"
181 brdSurf, _ := brd.Draw(ctx)
182 root.AddChild(0, ah, brdSurf)
183 ah += int(brdSurf.Size.Height)
184
185 if err != nil {
186 txt := text.New(fmt.Sprintf("Error: %s", err.Error()))
187 txt.Style = vaxis.Style{Foreground: red}
188 txtSurf, _ := txt.Draw(ctx)
189 root.AddChild(0, ah, txtSurf)
190 ah += int(txtSurf.Size.Height)
191 } else if logsLen == 0 {
192 txt := text.New("No access logs found.")
193 txtSurf, _ := txt.Draw(ctx)
194 root.AddChild(0, ah, txtSurf)
195 ah += int(txtSurf.Size.Height)
196 } else if filteredLen == 0 {
197 txt := text.New("No logs match filter.")
198 txtSurf, _ := txt.Draw(ctx)
199 root.AddChild(0, ah, txtSurf)
200 ah += int(txtSurf.Size.Height)
201 } else {
202 listPane := NewBorder(m.list)
203 listPane.Label = "access logs"
204 m.focusBorder(listPane)
205 listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)-3))
206 root.AddChild(0, ah, listSurf)
207 ah += int(listSurf.Size.Height)
208 }
209
210 inp, _ := m.input.Draw(createDrawCtx(ctx, 4))
211 root.AddChild(0, ah, inp)
212
213 return root, nil
214}
215
216func (m *AccessLogsPage) focusBorder(brd *Border) {
217 focus := m.focus
218 if focus == brd.Label {
219 brd.Style = vaxis.Style{Foreground: oj}
220 } else {
221 brd.Style = vaxis.Style{Foreground: purp}
222 }
223}
224
225func (m *AccessLogsPage) getWidget(i uint, cursor uint) vxfw.Widget {
226 if len(m.filtered) == 0 {
227 return nil
228 }
229
230 if int(i) >= len(m.filtered) {
231 return nil
232 }
233
234 idx := m.filtered[i]
235 return accessLogToWidget(m.logs[idx])
236}
237
238func accessLogToWidget(log *db.AccessLog) vxfw.Widget {
239 dateStr := ""
240 if log.CreatedAt != nil {
241 dateStr = log.CreatedAt.Format(time.RFC3339)
242 }
243
244 segs := []vaxis.Segment{
245 {Text: dateStr + " "},
246 {Text: log.Service + " ", Style: vaxis.Style{Foreground: purp}},
247 {Text: log.Identity + " ", Style: vaxis.Style{Foreground: green}},
248 }
249
250 if log.Pubkey != "" {
251 fingerprint := log.Pubkey
252 if len(fingerprint) > 16 {
253 fingerprint = fingerprint[0:25] + "..." + fingerprint[len(fingerprint)-15:]
254 }
255 segs = append(segs, vaxis.Segment{
256 Text: fingerprint,
257 Style: vaxis.Style{Foreground: grey},
258 })
259 }
260
261 txt := richtext.New(segs)
262 return txt
263}