okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petRestore the webhooks shortcut and keep agent access visiblef51cc1a

main
5.6 KiB159 linesraw
1package tui
2
3import (
4	"cmp"
5	"fmt"
6)
7
8// View draws the list beside the highlighted hook's detail. The form and the
9// secret shown once take the list's place, and a question takes the footer.
10func (m WebhooksPageModel) View() string {
11	lv := m.listView
12	lv.Empty = "No webhooks yet. Add one to wake a session with a signed request."
13	switch {
14	case m.Loading:
15		lv.Empty = "loading webhooks…"
16	case !m.Loaded:
17		lv.Empty = "webhooks did not load"
18	}
19	frame := lv.frame(brand("albedo")+" "+DefaultStyles.Muted.Render("/webhooks"), DefaultStyles.Faint.Render("all sessions"), m.footer(max(1, m.Width)))
20	switch {
21	case m.Reveal != nil:
22		frame.filter, frame.pane, frame.list = "", nil, m.revealRows
23	case m.Form != nil:
24		frame.filter, frame.pane, frame.list = "", nil, m.formRows
25	}
26	return frame.view(m.Width, m.Height)
27}
28
29// footer is the keys for what is open and one status: a failure, a notice,
30// work in flight, or what the hooks are doing.
31func (m WebhooksPageModel) footer(width int) string {
32	switch {
33	case m.Reveal != nil:
34		status, urgent := m.status()
35		return footerLine(width, []hint{{"ctrl+l", "copy"}, {"enter", "done"}}, status, urgent)
36	case m.Form != nil:
37		return m.formFooter(width)
38	case m.confirm.asking():
39		return m.confirm.footer(width, m.Error)
40	}
41	status, urgent := m.status()
42	return footerLine(width, m.browseHints(), status, urgent)
43}
44
45// status is the footer's state beside the keys. An urgent status takes a row
46// of its own when the keys do not leave room for it.
47func (m WebhooksPageModel) status() (string, bool) {
48	switch {
49	case m.Error != "":
50		return DefaultStyles.Error.Render(m.Error), true
51	case m.Saving:
52		return DefaultStyles.Busy.Render("saving…"), true
53	case m.Loading:
54		return DefaultStyles.Faint.Render("loading…"), false
55	case m.Notice != "":
56		return DefaultStyles.Warning.Render(m.Notice), true
57	case !m.Mounted:
58		return DefaultStyles.Warning.Render("Webhooks are not listening · enable them in /extensions"), true
59	}
60	return "", false
61}
62
63// browseHints keeps the main hook action and session agent access visible first.
64func (m WebhooksPageModel) browseHints() []hint {
65	if !m.Loaded {
66		return []hint{{"ctrl+r", "retry"}, {"esc", "back"}}
67	}
68	_, selected := m.current()
69	var hints []hint
70	if selected {
71		hints = append(hints, hint{"enter", "on/off"})
72	}
73	hints = append(hints, hint{"ctrl+t", "agent access " + onOff(m.AgentManagement)}, hint{"ctrl+o", "add hook"})
74	if selected {
75		hints = append(hints, hint{"ctrl+e", "edit"}, hint{"ctrl+d", "delete"}, hint{"ctrl+g", "new secret"}, hint{"ctrl+l", "copy url"})
76	}
77	return append(hints, hint{"ctrl+r", "refresh"}, hint{"esc", "back"})
78}
79
80// formFooter is the form's keys, with a failure or the save in flight above them.
81func (m WebhooksPageModel) formFooter(width int) string {
82	line := m.Form.footer(width)
83	switch {
84	case m.Error != "":
85		return footerLine(width, nil, DefaultStyles.Error.Render(m.Error), true) + "\n" + line
86	case m.Saving:
87		return footerLine(width, nil, DefaultStyles.Busy.Render("saving…"), true) + "\n" + line
88	}
89	return line
90}
91
92// formRows is the form in the list's place, scrolled to keep its focused field in view.
93func (m WebhooksPageModel) formRows(width, height int) []string {
94	return scrolled(m.Form.view(width), 1+m.Form.Focus, height)
95}
96
97// revealRows is the generated secret, shown once, in the list's place. Its
98// session and warning sit on rows of their own so a narrow terminal keeps them.
99func (m WebhooksPageModel) revealRows(_, _ int) []string {
100	r := m.Reveal
101	return []string{
102		DefaultStyles.Bold.Render("signing secret for " + r.Hook),
103		DefaultStyles.Faint.Render("→ " + sessionLabel(m.Sessions, r.Session, m.SessionID)),
104		DefaultStyles.Warning.Render("shown only once; copy it now"),
105		"",
106		"  " + DefaultStyles.Prompt.Render(r.Secret),
107	}
108}
109
110// entry is how a hook reads in the list: its state in front, its URL under
111// its name, and the deliveries waiting for its session at the edge.
112func (m WebhooksPageModel) entry(hook webhookEntry) listEntry {
113	lead, tag := DefaultStyles.Faint.Render("off"), ""
114	if hook.Enabled {
115		lead = DefaultStyles.Success.Render("on")
116	}
117	if hook.Queued > 0 {
118		tag = DefaultStyles.Warning.Render(fmt.Sprintf("%d queued", hook.Queued))
119	}
120	section := sessionLabel(m.Sessions, hook.Session, m.SessionID)
121	return listEntry{
122		key:     hook.ID,
123		section: section,
124		lead:    lead,
125		name:    hook.Name,
126		desc:    hook.URL,
127		tag:     tag,
128		search:  []string{section},
129		detail:  func(width int) []string { return m.details(hook, width) },
130	}
131}
132
133// details is the pane: the state, where deliveries go, how they are signed,
134// and what is waiting for the session.
135func (m WebhooksPageModel) details(hook webhookEntry, width int) []string {
136	state := "off · new deliveries answer 404"
137	if hook.Enabled {
138		state = "on · accepting signed deliveries"
139	}
140	lines := paneTitle(hook.Name, state, width)
141	lines = append(lines, factRows("wakes", sessionLabel(m.Sessions, hook.Session, m.SessionID), width)...)
142	lines = append(lines, factRows("url", "POST "+cmp.Or(hook.Address, hook.URL), width)...)
143	lines = append(lines, factRows("signature", hook.Header+": "+hook.Prefix+"<hex HMAC-SHA256 of the body>", width)...)
144	lines = append(lines, factRows("secret", "stored · ctrl+g makes a new one", width)...)
145	inbox := "empty"
146	if hook.Queued > 0 {
147		inbox = fmt.Sprintf("%d waiting for the session", hook.Queued)
148		if hook.Deferred != "" {
149			inbox += " · last attempt: " + hook.Deferred
150		}
151	}
152	return append(lines, factRows("inbox", inbox, width)...)
153}
154
155// scrolled is the window of rows lines that keeps line at in view.
156func scrolled(lines []string, at, rows int) []string {
157	start := max(0, at-rows+1)
158	return lines[start:min(len(lines), start+rows)]
159}