- commit
- 13076a6
- parent
- 0417ee1
- author
- Eric Bower
- date
- 2025-03-19 18:54:07 -0400 EDT
feat(tui.tuns): conn events
9 files changed,
+282,
-35
M
Makefile
+2,
-1
1@@ -126,10 +126,11 @@ migrate:
2 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql
3 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql
4 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241202_add_more_idx_analytics.sql
5+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250319_add_tuns_event_logs_table.sql
6 .PHONY: migrate
7
8 latest:
9- $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241202_add_more_idx_analytics.sql
10+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250319_add_tuns_event_logs_table.sql
11 .PHONY: latest
12
13 psql:
+10,
-10
1@@ -112,12 +112,12 @@ successfully added pico+ user
2 </author>
3 </entry>
4 <entry>
5- <title>pico+ 1-month expiration notice</title>
6- <updated>2021-07-16T14:30:45Z</updated>
7- <id>1626445845</id>
8- <content type="html">
<html>
	<head>
		<title>pico+ 1-month expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-month expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</content>
9+ <title>pico+ 1-day expiration notice</title>
10+ <updated>2021-08-14T14:30:45Z</updated>
11+ <id>1628951445</id>
12+ <content type="html">
<html>
	<head>
		<title>pico+ 1-day expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-day expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</content>
13 <link href="https://pico.sh" rel="alternate"></link>
14- <summary type="html">
<html>
	<head>
		<title>pico+ 1-month expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-month expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</summary>
15+ <summary type="html">
<html>
	<head>
		<title>pico+ 1-day expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-day expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</summary>
16 <author>
17 <name>team pico</name>
18 </author>
19@@ -134,12 +134,12 @@ successfully added pico+ user
20 </author>
21 </entry>
22 <entry>
23- <title>pico+ 1-day expiration notice</title>
24- <updated>2021-08-14T14:30:45Z</updated>
25- <id>1628951445</id>
26- <content type="html">
<html>
	<head>
		<title>pico+ 1-day expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-day expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</content>
27+ <title>pico+ 1-month expiration notice</title>
28+ <updated>2021-07-16T14:30:45Z</updated>
29+ <id>1626445845</id>
30+ <content type="html">
<html>
	<head>
		<title>pico+ 1-month expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-month expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</content>
31 <link href="https://pico.sh" rel="alternate"></link>
32- <summary type="html">
<html>
	<head>
		<title>pico+ 1-day expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-day expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</summary>
33+ <summary type="html">
<html>
	<head>
		<title>pico+ 1-month expiration notification!</title>
		<style>
			code {
				background-color: #ddd;
				border-radius: 5px;
				padding: 1px 3px;
			}
		</style>
	</head>
	<body>
		<h1>pico+ 1-month expiration notification!</h1>
<p>
	Your <code>pico+</code> membership will expire on <strong>2021-08-16</strong>.
</p>
<p>
	If your pico+ membership expires then we will:

	<ul>
		<li>revoke access to <a href="https">https://tuns.sh</a></li>
		<li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
		<li>revoke access to our IRC bouncer</li>
	</ul>
</p>
<p>
	In order to continue using our premium services, you need to purchase another year:
	<a href="https://auth.pico.sh/checkout/user-a">purchase pico+</a>
</p>
<p>
	If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
</p>
	</body>
</html>
</summary>
34 <author>
35 <name>team pico</name>
36 </author>
+36,
-0
1@@ -20,6 +20,7 @@ import (
2 "github.com/picosh/pico/pkg/db/postgres"
3 "github.com/picosh/pico/pkg/shared"
4 "github.com/picosh/utils"
5+ "github.com/picosh/utils/pipe"
6 "github.com/picosh/utils/pipe/metrics"
7 "github.com/prometheus/client_golang/prometheus/promhttp"
8 )
9@@ -723,6 +724,39 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr
10 }
11 }
12
13+func tunsEventLogDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) {
14+ drain := pipe.NewReconnectReadWriteCloser(
15+ ctx,
16+ logger,
17+ shared.NewPicoPipeClient(),
18+ "tuns-event-drain-sub",
19+ "sub tuns-event-drain -k",
20+ 100,
21+ 10*time.Millisecond,
22+ )
23+
24+ for {
25+ scanner := bufio.NewScanner(drain)
26+ scanner.Buffer(make([]byte, 32*1024), 32*1024)
27+ for scanner.Scan() {
28+ line := scanner.Text()
29+ clean := strings.TrimSpace(line)
30+ var log db.TunsEventLog
31+ err := json.Unmarshal([]byte(clean), &log)
32+ if err != nil {
33+ logger.Error("could not unmarshal line", "err", err)
34+ continue
35+ }
36+
37+ logger.Info("inserting tuns event log", "log", log)
38+ err = dbpool.InsertTunsEventLog(&log)
39+ if err != nil {
40+ logger.Error("could not insert tuns event log", "err", err)
41+ }
42+ }
43+ }
44+}
45+
46 func authMux(apiConfig *shared.ApiConfig) *http.ServeMux {
47 serverRoot, err := fs.Sub(embedFS, "public")
48 if err != nil {
49@@ -792,6 +826,8 @@ func StartApiServer() {
50
51 // gather metrics in the auth service
52 go metricDrainSub(ctx, db, logger, cfg.Secret)
53+ // gather connect/disconnect logs from tuns
54+ go tunsEventLogDrainSub(ctx, db, logger, cfg.Secret)
55
56 defer ctx.Done()
57
+18,
-0
1@@ -323,6 +323,20 @@ type UserServiceStats struct {
2 LatestUpdatedAt time.Time
3 }
4
5+type TunsEventLog struct {
6+ ID string `json:"id"`
7+ ServerID string `json:"server_id"`
8+ Time *time.Time `json:"time"`
9+ User string `json:"user"`
10+ UserId string `json:"user_id"`
11+ RemoteAddr string `json:"remote_addr"`
12+ EventType string `json:"event_type"`
13+ TunnelType string `json:"tunnel_type"`
14+ ConnectionType string `json:"connection_type"`
15+ TunnelAddrs []string `json:"tunnel_addrs"`
16+ CreatedAt *time.Time `json:"created_at"`
17+}
18+
19 var NameValidator = regexp.MustCompile("^[a-zA-Z0-9]{1,50}$")
20 var DenyList = []string{
21 "admin",
22@@ -415,5 +429,9 @@ type DB interface {
23
24 FindUserStats(userID string) (*UserStats, error)
25
26+ InsertTunsEventLog(log *TunsEventLog) error
27+ FindTunsEventLogs(userID string) ([]*TunsEventLog, error)
28+ FindTunsEventLogsByAddr(userID, addr string) ([]*TunsEventLog, error)
29+
30 Close() error
31 }
+72,
-1
1@@ -12,7 +12,7 @@ import (
2
3 "slices"
4
5- _ "github.com/lib/pq"
6+ "github.com/lib/pq"
7 "github.com/picosh/pico/pkg/db"
8 "github.com/picosh/utils"
9 )
10@@ -1796,6 +1796,77 @@ func (me *PsqlDB) findPagesStats(userID string) (*db.UserServiceStats, error) {
11 return &stats, nil
12 }
13
14+func (me *PsqlDB) InsertTunsEventLog(log *db.TunsEventLog) error {
15+ _, err := me.Db.Exec(
16+ `INSERT INTO tuns_event_logs
17+ (user_id, server_id, remote_addr, event_type, tunnel_type, connection_type, tunnel_addrs)
18+ VALUES
19+ ($1, $2, $3, $4, $5, $6, $7)`,
20+ log.UserId, log.ServerID, log.RemoteAddr, log.EventType, log.TunnelType,
21+ log.ConnectionType, pq.Array(log.TunnelAddrs),
22+ )
23+ return err
24+}
25+
26+func (me *PsqlDB) FindTunsEventLogsByAddr(userID, addr string) ([]*db.TunsEventLog, error) {
27+ logs := []*db.TunsEventLog{}
28+ fmt.Println(addr)
29+ rs, err := me.Db.Query(
30+ `SELECT id, user_id, server_id, remote_addr, event_type, tunnel_type, connection_type, tunnel_addrs, created_at
31+ FROM tuns_event_logs WHERE user_id=$1 AND tunnel_addrs @> ARRAY[$2] ORDER BY created_at DESC`, userID, addr)
32+ if err != nil {
33+ return nil, err
34+ }
35+
36+ for rs.Next() {
37+ log := db.TunsEventLog{}
38+ err := rs.Scan(
39+ &log.ID, &log.UserId, &log.ServerID, &log.RemoteAddr,
40+ &log.EventType, &log.TunnelType, &log.ConnectionType,
41+ (*pq.StringArray)(&log.TunnelAddrs), &log.CreatedAt,
42+ )
43+ if err != nil {
44+ return nil, err
45+ }
46+ logs = append(logs, &log)
47+ }
48+
49+ if rs.Err() != nil {
50+ return nil, rs.Err()
51+ }
52+
53+ return logs, nil
54+}
55+
56+func (me *PsqlDB) FindTunsEventLogs(userID string) ([]*db.TunsEventLog, error) {
57+ logs := []*db.TunsEventLog{}
58+ rs, err := me.Db.Query(
59+ `SELECT id, user_id, server_id, remote_addr, event_type, tunnel_type, connection_type, tunnel_addrs, created_at
60+ FROM tuns_event_logs WHERE user_id=$1 ORDER BY created_at DESC`, userID)
61+ if err != nil {
62+ return nil, err
63+ }
64+
65+ for rs.Next() {
66+ log := db.TunsEventLog{}
67+ err := rs.Scan(
68+ &log.ID, &log.UserId, &log.ServerID, &log.RemoteAddr,
69+ &log.EventType, &log.TunnelType, &log.ConnectionType,
70+ (*pq.StringArray)(&log.TunnelAddrs), &log.CreatedAt,
71+ )
72+ if err != nil {
73+ return nil, err
74+ }
75+ logs = append(logs, &log)
76+ }
77+
78+ if rs.Err() != nil {
79+ return nil, rs.Err()
80+ }
81+
82+ return logs, nil
83+}
84+
85 func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) {
86 stats := db.UserStats{}
87 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)
+12,
-0
1@@ -268,3 +268,15 @@ func (me *StubDB) FindTagsForUser(userID string, tag string) ([]string, error) {
2 func (me *StubDB) FindUserStats(userID string) (*db.UserStats, error) {
3 return nil, notImpl
4 }
5+
6+func (me *StubDB) InsertTunsEventLog(log *db.TunsEventLog) error {
7+ return notImpl
8+}
9+
10+func (me *StubDB) FindTunsEventLogsByAddr(userID, addr string) ([]*db.TunsEventLog, error) {
11+ return nil, notImpl
12+}
13+
14+func (me *StubDB) FindTunsEventLogs(userID string) ([]*db.TunsEventLog, error) {
15+ return nil, notImpl
16+}
1@@ -2,6 +2,8 @@ package shared
2
3 import (
4 "fmt"
5+ "sort"
6+ "strings"
7 "time"
8
9 "github.com/gorilla/feeds"
10@@ -128,6 +130,38 @@ func UserFeed(me db.DB, user *db.User, token string) (*feeds.Feed, error) {
11 }
12 }
13
14+ tunsLogs, _ := me.FindTunsEventLogs(user.ID)
15+ for _, eventLog := range tunsLogs {
16+ content := fmt.Sprintf(`Created At: %s
17+Event type: %s
18+Connection type: %s
19+Remote addr: %s
20+Tunnel type: %s
21+Tunnel addrs: %s
22+Server: %s`,
23+ eventLog.CreatedAt.Format(time.RFC3339), eventLog.EventType, eventLog.ConnectionType,
24+ eventLog.RemoteAddr, eventLog.TunnelType, eventLog.TunnelAddrs, eventLog.ServerID,
25+ )
26+ logItem := &feeds.Item{
27+ Id: fmt.Sprintf("%d", eventLog.CreatedAt.Unix()),
28+ Title: fmt.Sprintf(
29+ "%s tuns event for %s",
30+ eventLog.EventType, strings.Join(eventLog.TunnelAddrs, ", "),
31+ ),
32+ Link: &feeds.Link{Href: "https://pico.sh"},
33+ Content: content,
34+ Created: *eventLog.CreatedAt,
35+ Updated: *eventLog.CreatedAt,
36+ Description: content,
37+ Author: &feeds.Author{Name: "team pico"},
38+ }
39+ feedItems = append(feedItems, logItem)
40+ }
41+
42+ sort.Slice(feedItems, func(i, j int) bool {
43+ return feedItems[i].Created.After(feedItems[j].Created)
44+ })
45+
46 feed.Items = feedItems
47 return feed, nil
48 }
+81,
-23
1@@ -15,6 +15,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/list"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5+ "github.com/picosh/pico/pkg/db"
6 "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils/pipe"
8 )
9@@ -50,16 +51,15 @@ type TunsClientSimple struct {
10 }
11
12 type ResultLog struct {
13- ServerID string `json:"server_id"`
14- User string `json:"user"`
15- UserId string `json:"user_id"`
16- CurrentTime string `json:"current_time"`
17- StartTime time.Time `json:"start_time"`
18- StartTimePretty string `json:"start_time_pretty"`
19- RequestTime string `json:"request_time"`
20- RequestIP string `json:"request_ip"`
21- RequestMethod string `json:"request_method"`
22- // RequestURL string `json:"request_url"`
23+ ServerID string `json:"server_id"`
24+ User string `json:"user"`
25+ UserId string `json:"user_id"`
26+ CurrentTime string `json:"current_time"`
27+ StartTime time.Time `json:"start_time"`
28+ StartTimePretty string `json:"start_time_pretty"`
29+ RequestTime string `json:"request_time"`
30+ RequestIP string `json:"request_ip"`
31+ RequestMethod string `json:"request_method"`
32 OriginalRequestURI string `json:"original_request_uri"`
33 RequestHeaders map[string][]string `json:"request_headers"`
34 RequestBody string `json:"request_body"`
35@@ -70,6 +70,7 @@ type ResultLog struct {
36 TunnelType string `json:"tunnel_type"`
37 ConnectionType string `json:"connection_type"`
38 TunnelAddrs []string `json:"tunnel_addrs"`
39+ // RequestURL string `json:"request_url"`
40 }
41
42 type ResultLogLineLoaded struct {
43@@ -78,21 +79,25 @@ type ResultLogLineLoaded struct {
44
45 type TunsLoaded struct{}
46
47+type EventLogsLoaded struct{}
48+
49 type TunsPage struct {
50 shared *SharedModel
51
52- loading bool
53- err error
54- tuns []TunsClientSimple
55- selected string
56- focus string
57- leftPane list.Dynamic
58- rightPane *Pager
59- logs []*ResultLog
60- logList list.Dynamic
61- ctx context.Context
62- done context.CancelFunc
63- isAdmin bool
64+ loading bool
65+ err error
66+ tuns []TunsClientSimple
67+ selected string
68+ focus string
69+ leftPane list.Dynamic
70+ rightPane *Pager
71+ logs []*ResultLog
72+ logList list.Dynamic
73+ ctx context.Context
74+ done context.CancelFunc
75+ isAdmin bool
76+ eventLogs []*db.TunsEventLog
77+ eventLogList list.Dynamic
78 }
79
80 func NewTunsPage(shrd *SharedModel) *TunsPage {
81@@ -103,6 +108,7 @@ func NewTunsPage(shrd *SharedModel) *TunsPage {
82 }
83 m.leftPane = list.Dynamic{DrawCursor: true, Builder: m.getLeftWidget}
84 m.logList = list.Dynamic{DrawCursor: true, Builder: m.getLogWidget}
85+ m.eventLogList = list.Dynamic{DrawCursor: true, Builder: m.getEventLogWidget}
86 ff, _ := shrd.Dbpool.FindFeatureForUser(m.shared.User.ID, "admin")
87 if ff != nil {
88 m.isAdmin = true
89@@ -145,6 +151,25 @@ func (m *TunsPage) getLogWidget(i uint, cursor uint) vxfw.Widget {
90 return txt
91 }
92
93+func (m *TunsPage) getEventLogWidget(i uint, cursor uint) vxfw.Widget {
94+ if int(i) >= len(m.eventLogs) {
95+ return nil
96+ }
97+
98+ log := m.eventLogs[i]
99+ style := vaxis.Style{Foreground: green}
100+ if log.EventType == "disconnect" {
101+ style = vaxis.Style{Foreground: red}
102+ }
103+ txt := richtext.New([]vaxis.Segment{
104+ {Text: log.CreatedAt.Format(time.RFC3339) + " "},
105+ {Text: log.EventType + " ", Style: style},
106+ {Text: log.RemoteAddr},
107+ })
108+ txt.Softwrap = false
109+ return txt
110+}
111+
112 func (m *TunsPage) connectToLogs() error {
113 ctx, cancel := context.WithCancel(m.shared.Session.Context())
114 m.ctx = ctx
115@@ -208,6 +233,8 @@ func (m *TunsPage) HandleEvent(ev vaxis.Event, ph vxfw.EventPhase) (vxfw.Command
116 m.logList.SetCursor(uint(len(m.logs) - 1))
117 }
118 return vxfw.RedrawCmd{}, nil
119+ case EventLogsLoaded:
120+ return vxfw.RedrawCmd{}, nil
121 case TunsLoaded:
122 m.focus = "tuns"
123 return vxfw.BatchCmd([]vxfw.Command{
124@@ -218,6 +245,8 @@ func (m *TunsPage) HandleEvent(ev vaxis.Event, ph vxfw.EventPhase) (vxfw.Command
125 if msg.Matches(vaxis.KeyEnter) {
126 m.selected = m.tuns[m.leftPane.Cursor()].TunAddress
127 m.logs = []*ResultLog{}
128+ m.eventLogs = []*db.TunsEventLog{}
129+ go m.fetchEventLogs()
130 return vxfw.RedrawCmd{}, nil
131 }
132 if msg.Matches(vaxis.KeyTab) {
133@@ -325,7 +354,20 @@ func (m *TunsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
134 Characters: vaxis.Characters,
135 Max: vxfw.Size{
136 Width: uint16(rightPaneW) - 4,
137- Height: ctx.Max.Height - uint16(ah) - 3,
138+ Height: 15,
139+ },
140+ })
141+ rightSurf.AddChild(0, ah, surf)
142+ ah += int(surf.Size.Height)
143+
144+ brd = NewBorder(&m.eventLogList)
145+ brd.Label = "conn events"
146+ m.focusBorder(brd)
147+ surf, _ = brd.Draw(vxfw.DrawContext{
148+ Characters: vaxis.Characters,
149+ Max: vxfw.Size{
150+ Width: uint16(rightPaneW) - 4,
151+ Height: 15,
152 },
153 })
154 rightSurf.AddChild(0, ah, surf)
155@@ -376,6 +418,22 @@ func fetch(fqdn, auth string) (map[string]*TunsClient, error) {
156 return data.Clients, nil
157 }
158
159+func (m *TunsPage) fetchEventLogs() {
160+ site := m.findSelected()
161+ addr := m.selected
162+ if site.TunType == "http" {
163+ addr = "http://" + addr
164+ } else if site.TunType == "https" {
165+ addr = "https://" + addr
166+ }
167+ logs, err := m.shared.Dbpool.FindTunsEventLogsByAddr(m.shared.User.ID, addr)
168+ if err != nil {
169+ m.err = err
170+ return
171+ }
172+ m.eventLogs = logs
173+}
174+
175 func (m *TunsPage) fetchTuns() {
176 tMap, err := fetch("tuns.sh", m.shared.Cfg.TunsSecret)
177 if err != nil {
1@@ -0,0 +1,17 @@
2+CREATE TABLE IF NOT EXISTS tuns_event_logs (
3+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
4+ user_id uuid NOT NULL,
5+ server_id text NOT NULL,
6+ remote_addr text NOT NULL,
7+ event_type text NOT NULL,
8+ tunnel_type text NOT NULL,
9+ connection_type text NOT NULL,
10+ tunnel_addrs text[] NOT NULL,
11+ created_at timestamp without time zone NOT NULL DEFAULT NOW(),
12+ CONSTRAINT tuns_event_logs_pkey PRIMARY KEY (id),
13+ CONSTRAINT fk_tuns_event_logs_user
14+ FOREIGN KEY(user_id)
15+ REFERENCES app_users(id)
16+ ON DELETE CASCADE
17+ ON UPDATE CASCADE
18+);