feat: implement issue and PR detail handlers with routes

Add GET /issues/{owner}/{repo}/{index} and GET /pulls/{owner}/{repo}/{index}
routes that fetch individual issues/PRs from the Gitea API and render them
using the existing issue_detail.html and pull_detail.html templates.

New client methods:
- GetIssue: fetch a single issue by owner/repo/index
- GetPull: fetch a single pull request by owner/repo/index
- GetIssueComments: fetch comments for an issue
- GetRepoLabels: fetch available labels for a repository

Both handlers support HTMX fragment responses and full-page rendering,
consistent with the existing handler pattern.

Closes leeworks-agents/gitea-mobile#24

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent-company
2026-03-26 10:05:17 +00:00
parent 2367c19e42
commit 04e1f21405
2 changed files with 212 additions and 0 deletions
+115
View File
@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"strconv"
"strings"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config"
giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea"
@@ -40,8 +41,12 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /issues", h.CreateIssue)
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
// Issue detail.
mux.HandleFunc("GET /issues/{owner}/{repo}/{index}", h.IssueDetail)
// Pull requests.
mux.HandleFunc("GET /pulls", h.ListPulls)
mux.HandleFunc("GET /pulls/{owner}/{repo}/{index}", h.PullDetail)
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
// Settings (handled separately for auth bypass).
@@ -329,6 +334,116 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
renderPage(w, r, "Pull Requests", "pulls", content)
}
// IssueDetail handles GET /issues/{owner}/{repo}/{index}.
func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
token := getToken(r)
owner := r.PathValue("owner")
repo := r.PathValue("repo")
indexStr := r.PathValue("index")
index, err := strconv.ParseInt(indexStr, 10, 64)
if err != nil {
http.Error(w, "invalid issue index", http.StatusBadRequest)
return
}
issue, err := h.Client.GetIssue(r.Context(), token, owner, repo, index)
if err != nil {
slog.Error("failed to get issue", "error", err, "owner", owner, "repo", repo, "index", index)
renderPage(w, r, "Issue Not Found", "issues",
`<h1>Issue Not Found</h1><p class="empty">Could not load the requested issue.</p>`)
return
}
comments, err := h.Client.GetIssueComments(r.Context(), token, owner, repo, index)
if err != nil {
slog.Error("failed to get comments", "error", err)
comments = nil // non-fatal, render without comments
}
labels, err := h.Client.GetRepoLabels(r.Context(), token, owner, repo)
if err != nil {
slog.Error("failed to get repo labels", "error", err)
labels = nil
}
// Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/issue_detail.html")
if err != nil {
slog.Error("failed to parse issue_detail template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
type templateData struct {
Issue *giteaclient.Issue
Comments []giteaclient.Comment
AvailableLabels []giteaclient.Label
}
data := templateData{
Issue: issue,
Comments: comments,
AvailableLabels: labels,
}
var buf strings.Builder
if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
slog.Error("failed to execute issue_detail template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
renderPage(w, r, fmt.Sprintf("Issue #%d", index), "issues", buf.String())
}
// PullDetail handles GET /pulls/{owner}/{repo}/{index}.
func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
token := getToken(r)
owner := r.PathValue("owner")
repo := r.PathValue("repo")
indexStr := r.PathValue("index")
index, err := strconv.ParseInt(indexStr, 10, 64)
if err != nil {
http.Error(w, "invalid PR index", http.StatusBadRequest)
return
}
pr, err := h.Client.GetPull(r.Context(), token, owner, repo, index)
if err != nil {
slog.Error("failed to get pull request", "error", err, "owner", owner, "repo", repo, "index", index)
renderPage(w, r, "PR Not Found", "pulls",
`<h1>Pull Request Not Found</h1><p class="empty">Could not load the requested pull request.</p>`)
return
}
// Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/pull_detail.html")
if err != nil {
slog.Error("failed to parse pull_detail template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
type templateData struct {
Pull *giteaclient.PullRequest
}
data := templateData{
Pull: pr,
}
var buf strings.Builder
if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
slog.Error("failed to execute pull_detail template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
renderPage(w, r, fmt.Sprintf("PR #%d", index), "pulls", buf.String())
}
// CreateIssue handles POST /issues.
func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
token := getToken(r)