pico
created pr with
54.1
cmds
checkout latest patchset:
ssh pr.pico.sh print 54 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 54.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 54
Patchset
54.1
feat(tui.tuns): conn events
Eric Bower
2025-03-19T22:54:07ZSemantic diff summary
9 added,
8 modified,
0 signature changed,
0 removed
across 5 analyzed files
(2 files skipped: unsupported file type)
pkg/tui/tuns.go
-
chunklines 15-21modified -
type_declarationEventLogsLoadedadded -
type_declarationTunsPagemodified -
function_declarationNewTunsPagemodified -
method_declarationgetEventLogWidgetadded -
function_declarationHandleEventmodified -
function_declarationDrawmodified -
method_declarationfetchEventLogsadded
+2
-1
Makefile
#
| ... | ... | @@ -126,10 +126,11 @@ migrate: | |
| 126 | 126 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql | |
| 127 | 127 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql | |
| 128 | 128 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241202_add_more_idx_analytics.sql | |
| 129 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250319_add_tuns_event_logs_table.sql | |
| 129 | 130 | .PHONY: migrate | |
| 130 | 131 | ||
| 131 | 132 | latest: | |
| 132 | - | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241202_add_more_idx_analytics.sql | |
| 133 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250319_add_tuns_event_logs_table.sql | |
| 133 | 134 | .PHONY: latest | |
| 134 | 135 | ||
| 135 | 136 | psql: |
+35
-0
pkg/apps/auth/api.go
#
| ... | ... | @@ -723,6 +724,39 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr | |
| 723 | 724 | } | |
| 724 | 725 | } | |
| 725 | 726 | ||
| 727 | + | func tunsEventLogDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) { | |
| 728 | + | drain := pipe.NewReconnectReadWriteCloser( | |
| 729 | + | ctx, | |
| 730 | + | logger, | |
| 731 | + | shared.NewPicoPipeClient(), | |
| 732 | + | "tuns-event-drain-sub", | |
| 733 | + | "sub tuns-event-drain -k", | |
| 734 | + | 100, | |
| 735 | + | 10*time.Millisecond, | |
| 736 | + | ) | |
| 737 | + | ||
| 738 | + | for { | |
| 739 | + | scanner := bufio.NewScanner(drain) | |
| 740 | + | scanner.Buffer(make([]byte, 32*1024), 32*1024) | |
| 741 | + | for scanner.Scan() { | |
| 742 | + | line := scanner.Text() | |
| 743 | + | clean := strings.TrimSpace(line) | |
| 744 | + | var log db.TunsEventLog | |
| 745 | + | err := json.Unmarshal([]byte(clean), &log) | |
| 746 | + | if err != nil { | |
| 747 | + | logger.Error("could not unmarshal line", "err", err) | |
| 748 | + | continue | |
| 749 | + | } | |
| 750 | + | ||
| 751 | + | logger.Info("inserting tuns event log", "log", log) | |
| 752 | + | err = dbpool.InsertTunsEventLog(&log) | |
| 753 | + | if err != nil { | |
| 754 | + | logger.Error("could not insert tuns event log", "err", err) | |
| 755 | + | } | |
| 756 | + | } | |
| 757 | + | } | |
| 758 | + | } | |
| 759 | + | ||
| 726 | 760 | func authMux(apiConfig *shared.ApiConfig) *http.ServeMux { | |
| 727 | 761 | serverRoot, err := fs.Sub(embedFS, "public") | |
| 728 | 762 | if err != nil { |
+17
-0
pkg/db/db.go
#
| ... | ... | @@ -323,6 +323,20 @@ type UserServiceStats struct { | |
| 323 | 323 | LatestUpdatedAt time.Time | |
| 324 | 324 | } | |
| 325 | 325 | ||
| 326 | + | type TunsEventLog struct { | |
| 327 | + | ID string `json:"id"` | |
| 328 | + | ServerID string `json:"server_id"` | |
| 329 | + | Time *time.Time `json:"time"` | |
| 330 | + | User string `json:"user"` | |
| 331 | + | UserId string `json:"user_id"` | |
| 332 | + | RemoteAddr string `json:"remote_addr"` | |
| 333 | + | EventType string `json:"event_type"` | |
| 334 | + | TunnelType string `json:"tunnel_type"` | |
| 335 | + | ConnectionType string `json:"connection_type"` | |
| 336 | + | TunnelAddrs []string `json:"tunnel_addrs"` | |
| 337 | + | CreatedAt *time.Time `json:"created_at"` | |
| 338 | + | } | |
| 339 | + | ||
| 326 | 340 | var NameValidator = regexp.MustCompile("^[a-zA-Z0-9]{1,50}$") | |
| 327 | 341 | var DenyList = []string{ | |
| 328 | 342 | "admin", |
+39
-0
pkg/db/postgres/storage.go
#
| ... | ... | @@ -1796,6 +1796,45 @@ func (me *PsqlDB) findPagesStats(userID string) (*db.UserServiceStats, error) { | |
| 1796 | 1796 | return &stats, nil | |
| 1797 | 1797 | } | |
| 1798 | 1798 | ||
| 1799 | + | func (me *PsqlDB) InsertTunsEventLog(log *db.TunsEventLog) error { | |
| 1800 | + | _, err := me.Db.Exec( | |
| 1801 | + | `INSERT INTO tuns_event_logs | |
| 1802 | + | (user_id, server_id, remote_addr, tunnel_type, connection_type, tunnel_addrs) | |
| 1803 | + | VALUES | |
| 1804 | + | ($1, $2, $3, $4, $5, $6)`, | |
| 1805 | + | log.UserId, log.ServerID, log.RemoteAddr, log.TunnelType, log.ConnectionType, log.TunnelAddrs, | |
| 1806 | + | ) | |
| 1807 | + | return err | |
| 1808 | + | } | |
| 1809 | + | ||
| 1810 | + | func (me *PsqlDB) FindTunsEventLog(userID, addr string) ([]*db.TunsEventLog, error) { | |
| 1811 | + | logs := []*db.TunsEventLog{} | |
| 1812 | + | rs, err := me.Db.Query( | |
| 1813 | + | `SELECT user_id, server_id, remote_addr, tunnel_type, connection_type, tunnel_addrs, created_at | |
| 1814 | + | FROM tuns_event_logs WHERE user_id=$1 AND tunnel_addrs @> ARRAY[$2] ORDER BY created_at DESC`, userID, addr) | |
| 1815 | + | if err != nil { | |
| 1816 | + | return nil, err | |
| 1817 | + | } | |
| 1818 | + | ||
| 1819 | + | for rs.Next() { | |
| 1820 | + | log := db.TunsEventLog{} | |
| 1821 | + | err := rs.Scan( | |
| 1822 | + | &log.ID, &log.UserId, &log.ServerID, &log.RemoteAddr, | |
| 1823 | + | &log.TunnelType, &log.ConnectionType, &log.TunnelAddrs, &log.ConnectionType, &log.CreatedAt, | |
| 1824 | + | ) | |
| 1825 | + | if err != nil { | |
| 1826 | + | return nil, err | |
| 1827 | + | } | |
| 1828 | + | logs = append(logs, &log) | |
| 1829 | + | } | |
| 1830 | + | ||
| 1831 | + | if rs.Err() != nil { | |
| 1832 | + | return nil, rs.Err() | |
| 1833 | + | } | |
| 1834 | + | ||
| 1835 | + | return logs, nil | |
| 1836 | + | } | |
| 1837 | + | ||
| 1799 | 1838 | func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) { | |
| 1800 | 1839 | stats := db.UserStats{} | |
| 1801 | 1840 | rs, err := me.Db.Query(`SELECT cur_space, count(id), min(created_at), max(created_at), max(updated_at) FROM posts WHERE user_id=$1 GROUP BY cur_space`, userID) |
+8
-0
pkg/db/stub/stub.go
#
| ... | ... | @@ -268,3 +268,11 @@ func (me *StubDB) FindTagsForUser(userID string, tag string) ([]string, error) { | |
| 268 | 268 | func (me *StubDB) FindUserStats(userID string) (*db.UserStats, error) { | |
| 269 | 269 | return nil, notImpl | |
| 270 | 270 | } | |
| 271 | + | ||
| 272 | + | func (me *StubDB) InsertTunsEventLog(log *db.TunsEventLog) error { | |
| 273 | + | return notImpl | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (me *StubDB) FindTunsEventLog(userID, addr string) ([]*db.TunsEventLog, error) { | |
| 277 | + | return nil, notImpl | |
| 278 | + | } |
+63
-13
pkg/tui/tuns.go
#
| ... | ... | @@ -78,21 +79,25 @@ type ResultLogLineLoaded struct { | |
| 78 | 79 | ||
| 79 | 80 | type TunsLoaded struct{} | |
| 80 | 81 | ||
| 82 | + | type EventLogsLoaded struct{} | |
| 83 | + | ||
| 81 | 84 | type TunsPage struct { | |
| 82 | 85 | shared *SharedModel | |
| 83 | 86 | ||
| 84 | - | loading bool | |
| 85 | - | err error | |
| 86 | - | tuns []TunsClientSimple | |
| 87 | - | selected string | |
| 88 | - | focus string | |
| 89 | - | leftPane list.Dynamic | |
| 90 | - | rightPane *Pager | |
| 91 | - | logs []*ResultLog | |
| 92 | - | logList list.Dynamic | |
| 93 | - | ctx context.Context | |
| 94 | - | done context.CancelFunc | |
| 95 | - | isAdmin bool | |
| 87 | + | loading bool | |
| 88 | + | err error | |
| 89 | + | tuns []TunsClientSimple | |
| 90 | + | selected string | |
| 91 | + | focus string | |
| 92 | + | leftPane list.Dynamic | |
| 93 | + | rightPane *Pager | |
| 94 | + | logs []*ResultLog | |
| 95 | + | logList list.Dynamic | |
| 96 | + | ctx context.Context | |
| 97 | + | done context.CancelFunc | |
| 98 | + | isAdmin bool | |
| 99 | + | eventLogs []*db.TunsEventLog | |
| 100 | + | eventLogList list.Dynamic | |
| 96 | 101 | } | |
| 97 | 102 | ||
| 98 | 103 | func NewTunsPage(shrd *SharedModel) *TunsPage { |
| ... | ... | @@ -103,6 +108,7 @@ func NewTunsPage(shrd *SharedModel) *TunsPage { | |
| 103 | 108 | } | |
| 104 | 109 | m.leftPane = list.Dynamic{DrawCursor: true, Builder: m.getLeftWidget} | |
| 105 | 110 | m.logList = list.Dynamic{DrawCursor: true, Builder: m.getLogWidget} | |
| 111 | + | m.eventLogList = list.Dynamic{DrawCursor: true, Builder: m.getEventLogWidget} | |
| 106 | 112 | ff, _ := shrd.Dbpool.FindFeatureForUser(m.shared.User.ID, "admin") | |
| 107 | 113 | if ff != nil { | |
| 108 | 114 | m.isAdmin = true |
| ... | ... | @@ -145,6 +151,24 @@ func (m *TunsPage) getLogWidget(i uint, cursor uint) vxfw.Widget { | |
| 145 | 151 | return txt | |
| 146 | 152 | } | |
| 147 | 153 | ||
| 154 | + | func (m *TunsPage) getEventLogWidget(i uint, cursor uint) vxfw.Widget { | |
| 155 | + | if int(i) >= len(m.tuns) { | |
| 156 | + | return nil | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | log := m.eventLogs[i] | |
| 160 | + | style := vaxis.Style{Foreground: green} | |
| 161 | + | if log.EventType == "disconnect" { | |
| 162 | + | style = vaxis.Style{Foreground: red} | |
| 163 | + | } | |
| 164 | + | txt := richtext.New([]vaxis.Segment{ | |
| 165 | + | {Text: log.CreatedAt.Format(time.RFC3339) + " "}, | |
| 166 | + | {Text: log.EventType, Style: style}, | |
| 167 | + | }) | |
| 168 | + | txt.Softwrap = false | |
| 169 | + | return txt | |
| 170 | + | } | |
| 171 | + | ||
| 148 | 172 | func (m *TunsPage) connectToLogs() error { | |
| 149 | 173 | ctx, cancel := context.WithCancel(m.shared.Session.Context()) | |
| 150 | 174 | m.ctx = ctx |
| ... | ... | @@ -208,6 +232,8 @@ func (m *TunsPage) HandleEvent(ev vaxis.Event, ph vxfw.EventPhase) (vxfw.Command | |
| 208 | 232 | m.logList.SetCursor(uint(len(m.logs) - 1)) | |
| 209 | 233 | } | |
| 210 | 234 | return vxfw.RedrawCmd{}, nil | |
| 235 | + | case EventLogsLoaded: | |
| 236 | + | return vxfw.RedrawCmd{}, nil | |
| 211 | 237 | case TunsLoaded: | |
| 212 | 238 | m.focus = "tuns" | |
| 213 | 239 | return vxfw.BatchCmd([]vxfw.Command{ |
| ... | ... | @@ -218,6 +244,8 @@ func (m *TunsPage) HandleEvent(ev vaxis.Event, ph vxfw.EventPhase) (vxfw.Command | |
| 218 | 244 | if msg.Matches(vaxis.KeyEnter) { | |
| 219 | 245 | m.selected = m.tuns[m.leftPane.Cursor()].TunAddress | |
| 220 | 246 | m.logs = []*ResultLog{} | |
| 247 | + | m.eventLogs = []*db.TunsEventLog{} | |
| 248 | + | go m.fetchEventLogs() | |
| 221 | 249 | return vxfw.RedrawCmd{}, nil | |
| 222 | 250 | } | |
| 223 | 251 | if msg.Matches(vaxis.KeyTab) { |
| ... | ... | @@ -325,7 +353,20 @@ func (m *TunsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) { | |
| 325 | 353 | Characters: vaxis.Characters, | |
| 326 | 354 | Max: vxfw.Size{ | |
| 327 | 355 | Width: uint16(rightPaneW) - 4, | |
| 328 | - | Height: ctx.Max.Height - uint16(ah) - 3, | |
| 356 | + | Height: 15, | |
| 357 | + | }, | |
| 358 | + | }) | |
| 359 | + | rightSurf.AddChild(0, ah, surf) | |
| 360 | + | ah += int(surf.Size.Height) | |
| 361 | + | ||
| 362 | + | brd = NewBorder(&m.eventLogList) | |
| 363 | + | brd.Label = "conn events" | |
| 364 | + | m.focusBorder(brd) | |
| 365 | + | surf, _ = brd.Draw(vxfw.DrawContext{ | |
| 366 | + | Characters: vaxis.Characters, | |
| 367 | + | Max: vxfw.Size{ | |
| 368 | + | Width: uint16(rightPaneW) - 4, | |
| 369 | + | Height: 15, | |
| 329 | 370 | }, | |
| 330 | 371 | }) | |
| 331 | 372 | rightSurf.AddChild(0, ah, surf) |
| ... | ... | @@ -376,6 +417,15 @@ func fetch(fqdn, auth string) (map[string]*TunsClient, error) { | |
| 376 | 417 | return data.Clients, nil | |
| 377 | 418 | } | |
| 378 | 419 | ||
| 420 | + | func (m *TunsPage) fetchEventLogs() { | |
| 421 | + | logs, err := m.shared.Dbpool.FindTunsEventLog(m.shared.User.ID, m.selected) | |
| 422 | + | if err != nil { | |
| 423 | + | m.err = err | |
| 424 | + | return | |
| 425 | + | } | |
| 426 | + | m.eventLogs = logs | |
| 427 | + | } | |
| 428 | + | ||
| 379 | 429 | func (m *TunsPage) fetchTuns() { | |
| 380 | 430 | tMap, err := fetch("tuns.sh", m.shared.Cfg.TunsSecret) | |
| 381 | 431 | if err != nil { |
+17
-0
sql/migrations/20250319_add_tuns_event_logs_table.sql
#
| ... | ... | @@ -0,0 +1,17 @@ | |
| 1 | + | CREATE TABLE IF NOT EXISTS tuns_event_logs ( | |
| 2 | + | id uuid NOT NULL DEFAULT uuid_generate_v4(), | |
| 3 | + | user_id uuid NOT NULL, | |
| 4 | + | server_id text, | |
| 5 | + | remote_addr text, | |
| 6 | + | event_type text, | |
| 7 | + | tunnel_type text, | |
| 8 | + | connection_type text, | |
| 9 | + | tunnel_addrs text[], | |
| 10 | + | created_at timestamp without time zone NOT NULL DEFAULT NOW(), | |
| 11 | + | CONSTRAINT tuns_event_logs_pkey PRIMARY KEY (id), | |
| 12 | + | CONSTRAINT fk_tuns_event_logs_user | |
| 13 | + | FOREIGN KEY(user_id) | |
| 14 | + | REFERENCES app_users(id) | |
| 15 | + | ON DELETE CASCADE | |
| 16 | + | ON UPDATE CASCADE | |
| 17 | + | ); |