- 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
+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 }
+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)
1@@ -14,6 +14,7 @@ var menuChoices = []string{
2 "logs",
3 "access_logs",
4 "analytics",
5+ "pages",
6 "tuns",
7 "pico+",
8 "chat",
+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+}
+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,