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 var readmeRaw string
63 homeRepos := make([]HomeRepository, 0)
64 items := make([]repoItem, 0)
65
66 readmeRaw, readmeHTML, err = getServerReadme(ctx, be)
67 if err != nil {
68 logger.Debug("failed to get server README", "err", err)
69 }
70
71 for _, r := range repos {
72 if r.IsHidden() {
73 continue
74 }
75
76 al := be.AccessLevelByPublicKey(ctx, r.Name(), nil)
77 if al >= access.ReadOnlyAccess {
78 var lastUpdate *time.Time
79 lu := r.UpdatedAt()
80 if !lu.IsZero() {
81 lastUpdate = &lu
82 }
83 items = append(items, repoItem{
84 repo: r,
85 lastUpdate: lastUpdate,
86 })
87 }
88 }
89
90 sort.Slice(items, func(i, j int) bool {
91 if items[i].lastUpdate == nil && items[j].lastUpdate != nil {
92 return false
93 }
94 if items[i].lastUpdate != nil && items[j].lastUpdate == nil {
95 return true
96 }
97 if items[i].lastUpdate == nil && items[j].lastUpdate == nil {
98 return items[i].repo.Name() < items[j].repo.Name()
99 }
100 return items[i].lastUpdate.After(*items[j].lastUpdate)
101 })
102
103 page := 1
104 if pageStr := r.URL.Query().Get("page"); pageStr != "" {
105 if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
106 page = p
107 }
108 }
109
110 totalRepos := len(items)
111 totalPages := int(math.Ceil(float64(totalRepos) / float64(defaultReposPerPage)))
112 if totalPages < 1 {
113 totalPages = 1
114 }
115
116 if page > totalPages {
117 page = totalPages
118 }
119 if page < 1 {
120 page = 1
121 }
122
123 startIdx := (page - 1) * defaultReposPerPage
124 endIdx := startIdx + defaultReposPerPage
125 if endIdx > totalRepos {
126 endIdx = totalRepos
127 }
128
129 paginatedItems := items[startIdx:endIdx]
130
131 for _, item := range paginatedItems {
132 repo := item.repo
133 name := repo.Name()
134 projectName := repo.ProjectName()
135 description := strings.TrimSpace(repo.Description())
136 cloneURL := common.RepoURL(cfg.SSH.PublicURL, name)
137
138 var updatedAt time.Time
139 if item.lastUpdate != nil {
140 updatedAt = item.lastUpdate.UTC()
141 }
142
143 homeRepos = append(homeRepos, HomeRepository{
144 Name: name,
145 ProjectName: projectName,
146 Description: description,
147 IsPrivate: repo.IsPrivate(),
148 CloneURL: cloneURL,
149 UpdatedAt: updatedAt,
150 })
151 }
152
153 // Generate description from README or fallback
154 description := extractPlainTextFromMarkdown(readmeRaw, 200)
155 if description == "" {
156 description = "Git repositories on " + cfg.Name
157 }
158
159 data := HomeData{
160 BaseData: BaseData{
161 ServerName: cfg.Name,
162 ActiveTab: "repositories",
163 Title: cfg.Name,
164 Description: description,
165 },
166 PaginationData: PaginationData{
167 Page: page,
168 TotalPages: totalPages,
169 HasPrevPage: page > 1,
170 HasNextPage: page < totalPages,
171 },
172 Repositories: homeRepos,
173 ReadmeHTML: readmeHTML,
174 }
175
176 renderHTML(w, "home.html", data)
177}