1package web
2
3import (
4 "html/template"
5 "math"
6 "net/http"
7 "sort"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/charmbracelet/log/v2"
13 "github.com/charmbracelet/soft-serve/pkg/access"
14 "github.com/charmbracelet/soft-serve/pkg/backend"
15 "github.com/charmbracelet/soft-serve/pkg/config"
16 "github.com/charmbracelet/soft-serve/pkg/proto"
17 "github.com/charmbracelet/soft-serve/pkg/ui/common"
18)
19
20type HomeRepository struct {
21 Name string
22 ProjectName string
23 Description string
24 IsPrivate bool
25 CloneURL string
26 UpdatedAt time.Time
27}
28
29type HomeData struct {
30 BaseData
31 PaginationData
32 Repo proto.Repository
33 Repositories []HomeRepository
34 ReadmeHTML template.HTML
35}
36
37type repoItem struct {
38 repo proto.Repository
39 lastUpdate *time.Time
40}
41
42func home(w http.ResponseWriter, r *http.Request) {
43 ctx := r.Context()
44 logger := log.FromContext(ctx)
45 cfg := config.FromContext(ctx)
46 be := backend.FromContext(ctx)
47
48 if be == nil {
49 logger.Debug("backend not found in context")
50 renderInternalServerError(w, r)
51 return
52 }
53
54 repos, err := be.Repositories(ctx)
55 if err != nil {
56 logger.Debug("failed to get repositories", "err", err)
57 renderInternalServerError(w, r)
58 return
59 }
60
61 var readmeHTML template.HTML
62 homeRepos := make([]HomeRepository, 0)
63 items := make([]repoItem, 0)
64
65 readmeHTML, err = getServerReadme(ctx, be)
66 if err != nil {
67 logger.Debug("failed to get server README", "err", err)
68 }
69
70 for _, r := range repos {
71 if r.IsHidden() {
72 continue
73 }
74
75 al := be.AccessLevelByPublicKey(ctx, r.Name(), nil)
76 if al >= access.ReadOnlyAccess {
77 var lastUpdate *time.Time
78 lu := r.UpdatedAt()
79 if !lu.IsZero() {
80 lastUpdate = &lu
81 }
82 items = append(items, repoItem{
83 repo: r,
84 lastUpdate: lastUpdate,
85 })
86 }
87 }
88
89 sort.Slice(items, func(i, j int) bool {
90 if items[i].lastUpdate == nil && items[j].lastUpdate != nil {
91 return false
92 }
93 if items[i].lastUpdate != nil && items[j].lastUpdate == nil {
94 return true
95 }
96 if items[i].lastUpdate == nil && items[j].lastUpdate == nil {
97 return items[i].repo.Name() < items[j].repo.Name()
98 }
99 return items[i].lastUpdate.After(*items[j].lastUpdate)
100 })
101
102 page := 1
103 if pageStr := r.URL.Query().Get("page"); pageStr != "" {
104 if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
105 page = p
106 }
107 }
108
109 totalRepos := len(items)
110 totalPages := int(math.Ceil(float64(totalRepos) / float64(defaultReposPerPage)))
111 if totalPages < 1 {
112 totalPages = 1
113 }
114
115 if page > totalPages {
116 page = totalPages
117 }
118 if page < 1 {
119 page = 1
120 }
121
122 startIdx := (page - 1) * defaultReposPerPage
123 endIdx := startIdx + defaultReposPerPage
124 if endIdx > totalRepos {
125 endIdx = totalRepos
126 }
127
128 paginatedItems := items[startIdx:endIdx]
129
130 for _, item := range paginatedItems {
131 repo := item.repo
132 name := repo.Name()
133 projectName := repo.ProjectName()
134 description := strings.TrimSpace(repo.Description())
135 cloneURL := common.RepoURL(cfg.SSH.PublicURL, name)
136
137 var updatedAt time.Time
138 if item.lastUpdate != nil {
139 updatedAt = item.lastUpdate.UTC()
140 }
141
142 homeRepos = append(homeRepos, HomeRepository{
143 Name: name,
144 ProjectName: projectName,
145 Description: description,
146 IsPrivate: repo.IsPrivate(),
147 CloneURL: cloneURL,
148 UpdatedAt: updatedAt,
149 })
150 }
151
152 data := HomeData{
153 BaseData: BaseData{
154 ServerName: cfg.Name,
155 ActiveTab: "repositories",
156 Title: cfg.Name,
157 },
158 PaginationData: PaginationData{
159 Page: page,
160 TotalPages: totalPages,
161 HasPrevPage: page > 1,
162 HasNextPage: page < totalPages,
163 },
164 Repositories: homeRepos,
165 ReadmeHTML: readmeHTML,
166 }
167
168 renderHTML(w, "home.html", data)
169}