repos / pico

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

commit
fda3e4e
parent
d6f553a
author
Eric Bower
date
2025-12-20 12:11:56 -0500 EST
feat(tui): new pages section showing all sites
5 files changed,  +211, -0
M pkg/apps/pgs/db/postgres.go
+7, -0
 1@@ -34,6 +34,13 @@ func NewDB(databaseUrl string, logger *slog.Logger) (*PgsPsqlDB, error) {
 2 	return d, nil
 3 }
 4 
 5+func NewDBWithConn(db *sqlx.DB, logger *slog.Logger) *PgsPsqlDB {
 6+	return &PgsPsqlDB{
 7+		Logger: logger,
 8+		Db:     db,
 9+	}
10+}
11+
12 func (me *PgsPsqlDB) Close() error {
13 	return me.Db.Close()
14 }
M pkg/apps/pico/ssh.go
+4, -0
 1@@ -8,6 +8,7 @@ import (
 2 	"time"
 3 
 4 	"git.sr.ht/~rockorager/vaxis"
 5+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 6 	"github.com/picosh/pico/pkg/db/postgres"
 7 	"github.com/picosh/pico/pkg/pssh"
 8 	"github.com/picosh/pico/pkg/send/auth"
 9@@ -54,6 +55,8 @@ func StartSshServer() {
10 		_ = dbpool.Close()
11 	}()
12 
13+	pgsDB := pgsdb.NewDBWithConn(dbpool.Db, cfg.Logger)
14+
15 	handler := NewUploadHandler(
16 		dbpool,
17 		cfg,
18@@ -100,6 +103,7 @@ func StartSshServer() {
19 						Session: sesh,
20 						Cfg:     cfg,
21 						Dbpool:  handler.DBPool,
22+						PgsDB:   pgsDB,
23 						Logger:  pssh.GetLogger(sesh),
24 					}
25 					return pssh.PtyMdw(createTui(shrd), 200*time.Millisecond)(next)(sesh)
M pkg/tui/menu.go
+1, -0
1@@ -14,6 +14,7 @@ var menuChoices = []string{
2 	"logs",
3 	"access_logs",
4 	"analytics",
5+	"pages",
6 	"tuns",
7 	"pico+",
8 	"chat",
A pkg/tui/pages.go
+196, -0
  1@@ -0,0 +1,196 @@
  2+package tui
  3+
  4+import (
  5+	"fmt"
  6+	"sort"
  7+	"strings"
  8+
  9+	"git.sr.ht/~rockorager/vaxis"
 10+	"git.sr.ht/~rockorager/vaxis/vxfw"
 11+	"git.sr.ht/~rockorager/vaxis/vxfw/list"
 12+	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 13+	"git.sr.ht/~rockorager/vaxis/vxfw/text"
 14+	"github.com/picosh/pico/pkg/db"
 15+)
 16+
 17+type PagesLoaded struct{}
 18+
 19+type PagesPage struct {
 20+	shared *SharedModel
 21+
 22+	list    *list.Dynamic
 23+	pages   []*db.Project
 24+	loading bool
 25+	err     error
 26+}
 27+
 28+func NewPagesPage(shrd *SharedModel) *PagesPage {
 29+	page := &PagesPage{
 30+		shared: shrd,
 31+	}
 32+	page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true, Gap: 1}
 33+	return page
 34+}
 35+
 36+func (m *PagesPage) Footer() []Shortcut {
 37+	return []Shortcut{
 38+		{Shortcut: "c", Text: "copy url"},
 39+		{Shortcut: "^r", Text: "refresh"},
 40+	}
 41+}
 42+
 43+func (m *PagesPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 44+	switch msg := ev.(type) {
 45+	case PageIn:
 46+		m.loading = true
 47+		go m.fetchPages()
 48+		return vxfw.FocusWidgetCmd(m.list), nil
 49+	case PagesLoaded:
 50+		return vxfw.RedrawCmd{}, nil
 51+	case vaxis.Key:
 52+		if msg.Matches('c') {
 53+			cursor := m.list.Cursor()
 54+			if int(cursor) < len(m.pages) {
 55+				project := m.pages[cursor]
 56+				url := fmt.Sprintf("https://%s-%s.pgs.sh", m.shared.User.Name, project.Name)
 57+				return vxfw.CopyToClipboardCmd(url), nil
 58+			}
 59+		}
 60+		if msg.Matches('r', vaxis.ModCtrl) {
 61+			m.loading = true
 62+			go m.fetchPages()
 63+			return vxfw.RedrawCmd{}, nil
 64+		}
 65+	}
 66+	return nil, nil
 67+}
 68+
 69+func (m *PagesPage) fetchPages() {
 70+	if m.shared.User == nil {
 71+		m.err = fmt.Errorf("no user found")
 72+		m.loading = false
 73+		m.shared.App.PostEvent(PagesLoaded{})
 74+		return
 75+	}
 76+
 77+	if m.shared.PgsDB == nil {
 78+		m.err = fmt.Errorf("pgs database not configured")
 79+		m.loading = false
 80+		m.shared.App.PostEvent(PagesLoaded{})
 81+		return
 82+	}
 83+
 84+	pages, err := m.shared.PgsDB.FindProjectsByUser(m.shared.User.ID)
 85+
 86+	m.loading = false
 87+	if err != nil {
 88+		m.err = err
 89+		m.pages = []*db.Project{}
 90+	} else {
 91+		m.err = nil
 92+		sort.Slice(pages, func(i, j int) bool {
 93+			return pages[i].Name < pages[j].Name
 94+		})
 95+		m.pages = pages
 96+	}
 97+
 98+	m.shared.App.PostEvent(PagesLoaded{})
 99+}
100+
101+func (m *PagesPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
102+	w := ctx.Max.Width
103+	h := ctx.Max.Height
104+	root := vxfw.NewSurface(w, h, m)
105+	ah := 0
106+
107+	pagesLen := len(m.pages)
108+	err := m.err
109+
110+	info := text.New("Pages deployed to pgs.  Each page is accessible via https://{user}-{project}.pgs.sh.  We do not have access to custom domains so those cannot be shown.")
111+	brd := NewBorder(info)
112+	brd.Label = "desc"
113+	brdSurf, _ := brd.Draw(ctx)
114+	root.AddChild(0, ah, brdSurf)
115+	ah += int(brdSurf.Size.Height)
116+
117+	if err != nil {
118+		txt := text.New(fmt.Sprintf("Error: %s", err.Error()))
119+		txt.Style = vaxis.Style{Foreground: red}
120+		txtSurf, _ := txt.Draw(ctx)
121+		root.AddChild(0, ah, txtSurf)
122+	} else if pagesLen == 0 {
123+		txt := text.New("No pages found.")
124+		txtSurf, _ := txt.Draw(ctx)
125+		root.AddChild(0, ah, txtSurf)
126+	} else {
127+		listPane := NewBorder(m.list)
128+		listPane.Label = "pages"
129+		listPane.Style = vaxis.Style{Foreground: oj}
130+		listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)))
131+		root.AddChild(0, ah, listSurf)
132+	}
133+
134+	return root, nil
135+}
136+
137+func (m *PagesPage) getWidget(i uint, cursor uint) vxfw.Widget {
138+	if int(i) >= len(m.pages) {
139+		return nil
140+	}
141+
142+	isSelected := i == cursor
143+	return pageToWidget(m.pages[i], m.shared.User.Name, isSelected)
144+}
145+
146+func pageToWidget(project *db.Project, username string, isSelected bool) vxfw.Widget {
147+	url := fmt.Sprintf("https://%s-%s.pgs.sh", username, project.Name)
148+
149+	updatedAt := ""
150+	if project.UpdatedAt != nil {
151+		updatedAt = project.UpdatedAt.Format("2006-01-02 15:04:05")
152+	}
153+
154+	labelStyle := vaxis.Style{Foreground: grey}
155+	if isSelected {
156+		labelStyle = vaxis.Style{Foreground: fuschia}
157+	}
158+
159+	segs := []vaxis.Segment{
160+		{Text: "URL: ", Style: labelStyle},
161+		{Text: url + "\n", Style: vaxis.Style{Foreground: green}},
162+
163+		{Text: "Updated: ", Style: labelStyle},
164+		{Text: updatedAt},
165+	}
166+
167+	if project.ProjectDir != project.Name {
168+		segs = append(segs,
169+			vaxis.Segment{Text: "\n"},
170+			vaxis.Segment{Text: "Links To: ", Style: labelStyle},
171+			vaxis.Segment{Text: project.ProjectDir, Style: vaxis.Style{Foreground: purp}},
172+		)
173+	}
174+
175+	if project.Acl.Type != "" && project.Acl.Type != "public" {
176+		aclStr := project.Acl.Type
177+		if len(project.Acl.Data) > 0 {
178+			aclStr += " (" + strings.Join(project.Acl.Data, ", ") + ")"
179+		}
180+		segs = append(segs,
181+			vaxis.Segment{Text: "\n"},
182+			vaxis.Segment{Text: "ACL: ", Style: labelStyle},
183+			vaxis.Segment{Text: aclStr, Style: vaxis.Style{Foreground: oj}},
184+		)
185+	}
186+
187+	if project.Blocked != "" {
188+		segs = append(segs,
189+			vaxis.Segment{Text: "\n"},
190+			vaxis.Segment{Text: "Blocked: ", Style: labelStyle},
191+			vaxis.Segment{Text: project.Blocked, Style: vaxis.Style{Foreground: red}},
192+		)
193+	}
194+
195+	txt := richtext.New(segs)
196+	return txt
197+}
M pkg/tui/ui.go
+3, -0
 1@@ -10,6 +10,7 @@ import (
 2 	"git.sr.ht/~rockorager/vaxis"
 3 	"git.sr.ht/~rockorager/vaxis/vxfw"
 4 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 5+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 6 	"github.com/picosh/pico/pkg/db"
 7 	"github.com/picosh/pico/pkg/pssh"
 8 	"github.com/picosh/pico/pkg/shared"
 9@@ -26,6 +27,7 @@ type SharedModel struct {
10 	Session            *pssh.SSHServerConnSession
11 	Cfg                *shared.ConfigSite
12 	Dbpool             db.DB
13+	PgsDB              pgsdb.PgsDB
14 	User               *db.User
15 	PlusFeatureFlag    *db.FeatureFlag
16 	BouncerFeatureFlag *db.FeatureFlag
17@@ -355,6 +357,7 @@ func NewTui(opts vaxis.Options, shrd *SharedModel) error {
18 		"chat":        NewChatPage(shrd),
19 		"tuns":        NewTunsPage(shrd),
20 		"access_logs": NewAccessLogsPage(shrd),
21+		"pages":       NewPagesPage(shrd),
22 	}
23 	root := &App{
24 		shared: shrd,