Compare commits

..

1 Commits

Author SHA1 Message Date
agent-company d433801da6 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) <noreply@anthropic.com>
2026-03-26 17:08:58 +00:00
4 changed files with 113 additions and 185 deletions
-39
View File
@@ -706,45 +706,6 @@ func (c *Client) SubmitReview(ctx context.Context, token, owner, repo string, in
return nil
}
// RenderMarkdown renders raw markdown text to HTML using the Gitea API.
// Falls back to the raw text if the API call fails.
func (c *Client) RenderMarkdown(ctx context.Context, token, text string) (string, error) {
payload, err := json.Marshal(map[string]string{
"Text": text,
"Mode": "gfm",
})
if err != nil {
return text, fmt.Errorf("marshaling markdown request: %w", err)
}
url := c.baseURL + "/api/v1/markdown"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(payload)))
if err != nil {
return text, fmt.Errorf("creating markdown request: %w", err)
}
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/html")
resp, err := c.httpClient.Do(req)
if err != nil {
return text, fmt.Errorf("executing markdown request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return text, fmt.Errorf("markdown API error %d", resp.StatusCode)
}
rendered, err := io.ReadAll(resp.Body)
if err != nil {
return text, fmt.Errorf("reading markdown response: %w", err)
}
return string(rendered), nil
}
// priorityScore returns a numeric score for sorting (lower = higher priority).
func priorityScore(labels []string) int {
for _, l := range labels {
+106 -135
View File
@@ -180,158 +180,155 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
token := getToken(r)
orgs := h.getUserOrgs(r)
if len(orgs) == 0 {
renderPage(w, r, "Dashboard", "dashboard",
`<h1>Dashboard</h1><p class="empty">No organizations found. Check your token permissions.</p>`)
return
type dashboardData struct {
Items []giteaclient.TriageItem
Error string
}
var data dashboardData
if len(orgs) == 0 {
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)
renderPage(w, r, "Dashboard", "dashboard",
`<h1>Dashboard</h1><p class="empty">Error loading triage queue.</p>`)
data.Error = "Error loading triage queue."
} else {
data.Items = queue
}
}
tmpl, err := template.ParseFiles("internal/templates/dashboard.html")
if err != nil {
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",
`<h1>Dashboard</h1><p class="empty">No items need attention. Nice work!</p>`)
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 := `<h1>Dashboard</h1>`
for _, item := range queue {
typeBadge := `<span class="type-badge type-issue">issue</span>`
if item.Type == "pull" {
typeBadge = `<span class="type-badge type-pull">PR</span>`
}
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(`<span class="label" style="color:%s;border:1px solid %s">%s</span>`, color, color, template.HTMLEscapeString(l))
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title">%s %s</div>
<div class="card-meta">%s/%s #%d %s</div>
</div>`, 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",
`<h1>Issues</h1><p class="empty">No organizations found.</p>`)
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)
renderPage(w, r, "Issues", "issues",
`<h1>Issues</h1><p class="empty">Error loading issues.</p>`)
data.Error = "Error loading issues."
} else {
data.Issues = issues
}
}
tmpl, err := template.ParseFiles("internal/templates/issues.html")
if err != nil {
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",
`<h1>Issues</h1><p class="empty">No open issues found.</p>`)
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 := `<h1>Issues</h1>`
for _, issue := range issues {
labels := ""
for _, l := range issue.Labels {
labels += fmt.Sprintf(`<span class="label" style="color:#%s;border:1px solid #%s">%s</span>`,
l.Color, l.Color, template.HTMLEscapeString(l.Name))
}
assignee := ""
if issue.Assignee != nil {
assignee = fmt.Sprintf(` &middot; %s`, template.HTMLEscapeString(issue.Assignee.Login))
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title">%s</div>
<div class="card-meta">%s/%s #%d %s%s</div>
</div>`, 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",
`<h1>Pull Requests</h1><p class="empty">No organizations found.</p>`)
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)
renderPage(w, r, "Pull Requests", "pulls",
`<h1>Pull Requests</h1><p class="empty">Error loading pull requests.</p>`)
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 parse pulls template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
if len(prs) == 0 {
renderPage(w, r, "Pull Requests", "pulls",
`<h1>Pull Requests</h1><p class="empty">No open pull requests found.</p>`)
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 := `<h1>Pull Requests</h1>`
for _, pr := range prs {
labels := ""
for _, l := range pr.Labels {
labels += fmt.Sprintf(`<span class="label" style="color:#%s;border:1px solid #%s">%s</span>`,
l.Color, l.Color, template.HTMLEscapeString(l.Name))
}
stats := fmt.Sprintf(`<span style="color:#3fb950">+%d</span> <span style="color:#f85149">-%d</span>`, pr.Additions, pr.Deletions)
mergeStatus := ""
if pr.Mergeable {
mergeStatus = `<span style="color:#3fb950;font-size:0.7rem;">mergeable</span>`
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title"><span class="type-badge type-pull">PR</span> %s</div>
<div class="card-meta">%s/%s #%d %s %s %s</div>
</div>`, 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}.
@@ -367,17 +364,6 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
labels = nil
}
// Render markdown body if present.
var renderedBody template.HTML
if issue.Body != "" {
rendered, err := h.Client.RenderMarkdown(r.Context(), token, issue.Body)
if err != nil {
slog.Warn("failed to render issue body markdown, using plain text", "error", err)
} else {
renderedBody = template.HTML(rendered)
}
}
// Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/issue_detail.html")
if err != nil {
@@ -388,14 +374,12 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
type templateData struct {
Issue *giteaclient.Issue
RenderedBody template.HTML
Comments []giteaclient.Comment
AvailableLabels []giteaclient.Label
}
data := templateData{
Issue: issue,
RenderedBody: renderedBody,
Comments: comments,
AvailableLabels: labels,
}
@@ -431,17 +415,6 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
return
}
// Render markdown body if present.
var renderedBody template.HTML
if pr.Body != "" {
rendered, err := h.Client.RenderMarkdown(r.Context(), token, pr.Body)
if err != nil {
slog.Warn("failed to render PR body markdown, using plain text", "error", err)
} else {
renderedBody = template.HTML(rendered)
}
}
// Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/pull_detail.html")
if err != nil {
@@ -452,12 +425,10 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
type templateData struct {
Pull *giteaclient.PullRequest
RenderedBody template.HTML
}
data := templateData{
Pull: pr,
RenderedBody: renderedBody,
}
var buf strings.Builder
+1 -3
View File
@@ -9,9 +9,7 @@
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
{{end}}
</div>
{{if .RenderedBody}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Issue.Body}}
{{if .Issue.Body}}
<div class="card-body">{{.Issue.Body}}</div>
{{end}}
</div>
+1 -3
View File
@@ -15,9 +15,7 @@
<span class="diff-del">-{{.Pull.Deletions}}</span>
{{if .Pull.Mergeable}}<span style="color:var(--accent-green);">Mergeable</span>{{end}}
</div>
{{if .RenderedBody}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Pull.Body}}
{{if .Pull.Body}}
<div class="card-body">{{.Pull.Body}}</div>
{{end}}
</div>