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	"github.com/dustin/go-humanize"
 19)
 20
 21type HomeRepository struct {
 22	Name        string
 23	ProjectName string
 24	Description string
 25	IsPrivate   bool
 26	CloneURL    string
 27	UpdatedAt   string
 28}
 29
 30type HomeData struct {
 31	BaseData
 32	PaginationData
 33	Repo         proto.Repository
 34	Repositories []HomeRepository
 35	ReadmeHTML   template.HTML
 36}
 37
 38type repoItem struct {
 39	repo       proto.Repository
 40	lastUpdate *time.Time
 41}
 42
 43func home(w http.ResponseWriter, r *http.Request) {
 44	ctx := r.Context()
 45	logger := log.FromContext(ctx)
 46	cfg := config.FromContext(ctx)
 47	be := backend.FromContext(ctx)
 48
 49	if be == nil {
 50		logger.Debug("backend not found in context")
 51		renderInternalServerError(w, r)
 52		return
 53	}
 54
 55	repos, err := be.Repositories(ctx)
 56	if err != nil {
 57		logger.Debug("failed to get repositories", "err", err)
 58		renderInternalServerError(w, r)
 59		return
 60	}
 61
 62	var readmeHTML template.HTML
 63	homeRepos := make([]HomeRepository, 0)
 64	items := make([]repoItem, 0)
 65
 66	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 string
139		if item.lastUpdate != nil {
140			updatedAt = humanize.Time(*item.lastUpdate)
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	data := HomeData{
154		BaseData: BaseData{
155			ServerName: cfg.Name,
156			ActiveTab:  "repositories",
157			Title:      cfg.Name,
158		},
159		PaginationData: PaginationData{
160			Page:        page,
161			TotalPages:  totalPages,
162			HasPrevPage: page > 1,
163			HasNextPage: page < totalPages,
164		},
165		Repositories: homeRepos,
166		ReadmeHTML:   readmeHTML,
167	}
168
169	renderHTML(w, "home.html", data)
170}