Compare commits

..

4 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
AI-Manager 919a91d6aa Merge pull request 'feat: implement issue and PR detail handlers' (#27) from feature/issue-pr-detail-handlers into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 11:04:40 +00:00
AI-Manager 3c9a947017 Merge pull request 'fix: replace $GITHUB_OUTPUT with inline env vars in CI' (#26) from fix/remove-github-output into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 11:04:28 +00:00
agent-company cc90857cf5 fix: replace $GITHUB_OUTPUT with inline env vars in CI workflow
Collapse Set image tag, Build Docker image, and Push image steps into
a single step that computes TAG inline, eliminating the dependency on
$GITHUB_OUTPUT which is not reliably available in Gitea Actions runners.
Also moves registry login before the build+push step for correct ordering.

Closes leeworks-agents/gitea-mobile#25

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:03:14 +00:00
2 changed files with 117 additions and 127 deletions
+8 -15
View File
@@ -24,25 +24,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set image tag
id: tag
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
SHA=$(echo ${{ gitea.sha }} | cut -c1-7)
echo "tag=${TIMESTAMP}-${SHA}" >> $GITHUB_OUTPUT
- name: Build Docker image
run: |
docker build -t gitea.leeworks.dev/0xwheatyz/gitea-mobile:${{ steps.tag.outputs.tag }} .
docker tag gitea.leeworks.dev/0xwheatyz/gitea-mobile:${{ steps.tag.outputs.tag }} \
gitea.leeworks.dev/0xwheatyz/gitea-mobile:latest
- name: Login to Gitea registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login gitea.leeworks.dev \
-u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Push image
- name: Build and push Docker image
run: |
docker push gitea.leeworks.dev/0xwheatyz/gitea-mobile:${{ steps.tag.outputs.tag }}
TIMESTAMP=$(date +%Y%m%d%H%M%S)
SHA=$(echo ${{ gitea.sha }} | cut -c1-7)
TAG="${TIMESTAMP}-${SHA}"
docker build -t gitea.leeworks.dev/0xwheatyz/gitea-mobile:${TAG} .
docker tag gitea.leeworks.dev/0xwheatyz/gitea-mobile:${TAG} \
gitea.leeworks.dev/0xwheatyz/gitea-mobile:latest
docker push gitea.leeworks.dev/0xwheatyz/gitea-mobile:${TAG}
docker push gitea.leeworks.dev/0xwheatyz/gitea-mobile:latest
+109 -112
View File
@@ -180,158 +180,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",
`<h1>Dashboard</h1><p class="empty">No organizations found. Check your token permissions.</p>`)
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",
`<h1>Dashboard</h1><p class="empty">Error loading triage queue.</p>`)
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)
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",
`<h1>Issues</h1><p class="empty">Error loading issues.</p>`)
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)
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",
`<h1>Pull Requests</h1><p class="empty">Error loading pull requests.</p>`)
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}.