From 40ce5577528db1ab4ad519fe0db50cc9f8003c70 Mon Sep 17 00:00:00 2001 From: agent-company Date: Thu, 26 Mar 2026 17:08:58 +0000 Subject: [PATCH] refactor: wire Dashboard, ListIssues, and ListPulls to use template files Replace inline fmt.Sprintf HTML generation in Dashboard, ListIssues, and ListPulls handlers with template.ParseFiles rendering of dashboard.html, issues.html, and pulls.html respectively. ListIssues now reads ?org= and ?state= query params to filter results. ListPulls now reads ?org= query param to filter results. Closes leeworks-agents/gitea-mobile#34 Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/handlers/handlers.go | 221 +++++++++++++++++----------------- 1 file changed, 109 insertions(+), 112 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index db4a16b..b67ad6b 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -182,158 +182,155 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { token := getToken(r) orgs := h.getUserOrgs(r) + type dashboardData struct { + Items []giteaclient.TriageItem + Error string + } + + var data dashboardData + if len(orgs) == 0 { - renderPage(w, r, "Dashboard", "dashboard", - `

Dashboard

No organizations found. Check your token permissions.

`) - return + data.Error = "No organizations found. Check your token permissions." + } else { + queue, err := h.Client.GetTriageQueue(r.Context(), token, orgs) + if err != nil { + slog.Error("failed to get triage queue", "error", err) + data.Error = "Error loading triage queue." + } else { + data.Items = queue + } } - queue, err := h.Client.GetTriageQueue(r.Context(), token, orgs) + tmpl, err := template.ParseFiles("internal/templates/dashboard.html") if err != nil { - slog.Error("failed to get triage queue", "error", err) - renderPage(w, r, "Dashboard", "dashboard", - `

Dashboard

Error loading triage queue.

`) + slog.Error("failed to parse dashboard template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - if len(queue) == 0 { - renderPage(w, r, "Dashboard", "dashboard", - `

Dashboard

No items need attention. Nice work!

`) + var buf strings.Builder + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + slog.Error("failed to execute dashboard template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - content := `

Dashboard

` - for _, item := range queue { - typeBadge := `issue` - if item.Type == "pull" { - typeBadge = `PR` - } - - labels := "" - for _, l := range item.Labels { - color := "#8b949e" - switch l { - case "P1": - color = "#f85149" - case "P2": - color = "#d29922" - case "P3": - color = "#58a6ff" - } - labels += fmt.Sprintf(`%s`, color, color, template.HTMLEscapeString(l)) - } - - content += fmt.Sprintf(`
-
%s %s
-
%s/%s #%d %s
-
`, typeBadge, template.HTMLEscapeString(item.Title), - template.HTMLEscapeString(item.RepoOwner), - template.HTMLEscapeString(item.RepoName), - item.Number, labels) - } - - renderPage(w, r, "Dashboard", "dashboard", content) + renderPage(w, r, "Dashboard", "dashboard", buf.String()) } // ListIssues handles GET /issues. func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { token := getToken(r) - orgs := h.getUserOrgs(r) + orgNames := h.getUserOrgs(r) - if len(orgs) == 0 { - renderPage(w, r, "Issues", "issues", - `

Issues

No organizations found.

`) - return + type issuesData struct { + Issues []giteaclient.Issue + Orgs []string + SelectedOrg string + SelectedState string + HasMore bool + NextPage int + Error string } - issues, err := h.Client.ListAllIssues(r.Context(), token, orgs) + selectedOrg := r.URL.Query().Get("org") + selectedState := r.URL.Query().Get("state") + if selectedState == "" { + selectedState = "open" + } + + data := issuesData{ + Orgs: orgNames, + SelectedOrg: selectedOrg, + SelectedState: selectedState, + } + + if len(orgNames) == 0 { + data.Error = "No organizations found." + } else { + // Filter to selected org if specified. + queryOrgs := orgNames + if selectedOrg != "" { + queryOrgs = []string{selectedOrg} + } + + issues, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs) + if err != nil { + slog.Error("failed to list issues", "error", err) + data.Error = "Error loading issues." + } else { + data.Issues = issues + } + } + + tmpl, err := template.ParseFiles("internal/templates/issues.html") if err != nil { - slog.Error("failed to list issues", "error", err) - renderPage(w, r, "Issues", "issues", - `

Issues

Error loading issues.

`) + slog.Error("failed to parse issues template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - if len(issues) == 0 { - renderPage(w, r, "Issues", "issues", - `

Issues

No open issues found.

`) + var buf strings.Builder + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + slog.Error("failed to execute issues template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - content := `

Issues

` - for _, issue := range issues { - labels := "" - for _, l := range issue.Labels { - labels += fmt.Sprintf(`%s`, - l.Color, l.Color, template.HTMLEscapeString(l.Name)) - } - - assignee := "" - if issue.Assignee != nil { - assignee = fmt.Sprintf(` · %s`, template.HTMLEscapeString(issue.Assignee.Login)) - } - - content += fmt.Sprintf(`
-
%s
-
%s/%s #%d %s%s
-
`, template.HTMLEscapeString(issue.Title), - template.HTMLEscapeString(issue.RepoOwner), - template.HTMLEscapeString(issue.RepoName), - issue.Number, labels, assignee) - } - - renderPage(w, r, "Issues", "issues", content) + renderPage(w, r, "Issues", "issues", buf.String()) } // ListPulls handles GET /pulls. func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) { token := getToken(r) - orgs := h.getUserOrgs(r) + orgNames := h.getUserOrgs(r) - if len(orgs) == 0 { - renderPage(w, r, "Pull Requests", "pulls", - `

Pull Requests

No organizations found.

`) - return + type pullsData struct { + Pulls []giteaclient.PullRequest + Orgs []string + SelectedOrg string + Error string } - prs, err := h.Client.ListAllPullRequests(r.Context(), token, orgs) + selectedOrg := r.URL.Query().Get("org") + + data := pullsData{ + Orgs: orgNames, + SelectedOrg: selectedOrg, + } + + if len(orgNames) == 0 { + data.Error = "No organizations found." + } else { + queryOrgs := orgNames + if selectedOrg != "" { + queryOrgs = []string{selectedOrg} + } + + prs, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs) + if err != nil { + slog.Error("failed to list pull requests", "error", err) + data.Error = "Error loading pull requests." + } else { + data.Pulls = prs + } + } + + tmpl, err := template.ParseFiles("internal/templates/pulls.html") if err != nil { - slog.Error("failed to list pull requests", "error", err) - renderPage(w, r, "Pull Requests", "pulls", - `

Pull Requests

Error loading pull requests.

`) + slog.Error("failed to parse pulls template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - if len(prs) == 0 { - renderPage(w, r, "Pull Requests", "pulls", - `

Pull Requests

No open pull requests found.

`) + var buf strings.Builder + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + slog.Error("failed to execute pulls template", "error", err) + http.Error(w, "template error", http.StatusInternalServerError) return } - content := `

Pull Requests

` - for _, pr := range prs { - labels := "" - for _, l := range pr.Labels { - labels += fmt.Sprintf(`%s`, - l.Color, l.Color, template.HTMLEscapeString(l.Name)) - } - - stats := fmt.Sprintf(`+%d -%d`, pr.Additions, pr.Deletions) - mergeStatus := "" - if pr.Mergeable { - mergeStatus = `mergeable` - } - - content += fmt.Sprintf(`
-
PR %s
-
%s/%s #%d %s %s %s
-
`, template.HTMLEscapeString(pr.Title), - template.HTMLEscapeString(pr.RepoOwner), - template.HTMLEscapeString(pr.RepoName), - pr.Number, labels, stats, mergeStatus) - } - - renderPage(w, r, "Pull Requests", "pulls", content) + renderPage(w, r, "Pull Requests", "pulls", buf.String()) } // IssueDetail handles GET /issues/{owner}/{repo}/{index}.