repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / tui
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}