Compare commits

...

16 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 04e1f21405 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>
2026-03-26 10:05:17 +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
AI-Manager 2367c19e42 Merge pull request 'fix: replace github.sha with gitea.sha in CI workflow' (#23) from fix/gitea-sha-ci-workflow into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 08:04:25 +00:00
agent-company abd879ab66 fix: replace github.sha with gitea.sha in CI workflow
The Gitea Actions workflow used ${{ github.sha }} which is GitHub Actions
syntax. In Gitea Actions the correct context variable is ${{ gitea.sha }}.
This caused the image tag SHA component to be empty.

Closes leeworks-agents/gitea-mobile#20

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 08:03:55 +00:00
AI-Manager eb1252f902 Merge pull request 'fix: vendor htmx.min.js locally instead of loading from CDN' (#19) from fix/vendor-htmx-locally into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 08:02:27 +00:00
agent-company 37ddfb128b fix: vendor htmx.min.js locally instead of loading from CDN
Download htmx.org v1.9.10 into static/htmx.min.js and update all
references (layout.html, handlers.go fallback page, sw.js precache
list) to use the local copy. This enables the PWA to work fully
offline since the service worker can now cache htmx from the same
origin.

Bump service worker cache version to v2 so existing installations
pick up the new asset list.

Closes leeworks-agents/gitea-mobile#17

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 06:03:46 +00:00
AI-Manager cf841ac5d9 Merge pull request 'feat: implement mobile-first HTMX templates and CSS' (#15) from feature/templates-css into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 05:05:32 +00:00
AI-Manager 43d621e688 Merge pull request 'feat: add PWA manifest and service worker' (#14) from feature/pwa into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 05:05:20 +00:00
AI-Manager 4a25f5fac4 Merge pull request 'feat: add Dockerfile and CI workflow' (#13) from feature/dockerfile-ci into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-26 05:05:14 +00:00
AI-Manager 180fd9b65c Merge pull request 'feat: add HTTP handlers and health endpoint' (#12) from feature/http-handlers into master 2026-03-26 05:05:01 +00:00
AI-Manager f464e11b00 Merge pull request 'feat: implement Gitea aggregation layer with concurrent fetching' (#11) from feature/gitea-aggregation into master 2026-03-26 05:04:46 +00:00
AI-Manager 24b44debf0 Merge pull request 'feat: add env-based configuration and token-in-cookie auth' (#10) from feature/config-auth into master 2026-03-26 05:04:23 +00:00
agent-company 86173b61eb feat: add Dockerfile and CI workflow
Add multi-stage Dockerfile producing a minimal distroless image and
Gitea Actions CI workflow for automated testing and image publishing.

- Dockerfile: multi-stage build (golang:1.22-alpine -> distroless/static)
  with stripped binary (~15-20MB image), runs as nonroot user
- .dockerignore: excludes .git, docs, nix files from build context
- .gitea/workflows/build.yaml: CI pipeline that runs tests, builds
  Docker image, and pushes to Gitea registry with timestamp+SHA tags
  for Flux image automation

Closes leeworks-agents/gitea-mobile#7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 04:10:59 +00:00
8 changed files with 387 additions and 112 deletions
+8
View File
@@ -0,0 +1,8 @@
.git
.gitignore
*.md
flake.nix
flake.lock
.envrc
.direnv
.claude
+41
View File
@@ -0,0 +1,41 @@
name: Build and Push
on:
push:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run tests
run: go test ./...
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Login to Gitea registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login gitea.leeworks.dev \
-u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build and push Docker image
run: |
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
+16
View File
@@ -0,0 +1,16 @@
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /gitea-mobile ./cmd/server
# Stage 2: Runtime
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /gitea-mobile /gitea-mobile
COPY static/ /static/
COPY internal/templates/ /templates/
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/gitea-mobile"]
+97
View File
@@ -530,6 +530,103 @@ func (c *Client) GetTriageQueue(ctx context.Context, token string, orgs []string
return queue, nil return queue, nil
} }
// Comment represents a comment on an issue or pull request.
type Comment struct {
ID int64 `json:"id"`
Body string `json:"body"`
User string `json:"-"` // populated from nested object
CreatedAt string `json:"-"` // formatted after fetch
RawUser struct {
Login string `json:"login"`
} `json:"user"`
RawCreatedAt time.Time `json:"created_at"`
}
// Label represents a Gitea label (used for available labels list).
type Label struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
}
// GetIssue fetches a single issue by owner, repo, and index.
func (c *Client) GetIssue(ctx context.Context, token, owner, repo string, index int64) (*Issue, error) {
path := fmt.Sprintf("/repos/%s/%s/issues/%d", owner, repo, index)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return nil, fmt.Errorf("fetching issue: %w", err)
}
defer resp.Body.Close()
var issue Issue
if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil {
return nil, fmt.Errorf("decoding issue: %w", err)
}
issue.RepoOwner = owner
issue.RepoName = repo
return &issue, nil
}
// GetPull fetches a single pull request by owner, repo, and index.
func (c *Client) GetPull(ctx context.Context, token, owner, repo string, index int64) (*PullRequest, error) {
path := fmt.Sprintf("/repos/%s/%s/pulls/%d", owner, repo, index)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return nil, fmt.Errorf("fetching pull request: %w", err)
}
defer resp.Body.Close()
var pr PullRequest
if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil {
return nil, fmt.Errorf("decoding pull request: %w", err)
}
pr.RepoOwner = owner
pr.RepoName = repo
return &pr, nil
}
// GetIssueComments fetches comments for an issue or pull request.
func (c *Client) GetIssueComments(ctx context.Context, token, owner, repo string, index int64) ([]Comment, error) {
path := fmt.Sprintf("/repos/%s/%s/issues/%d/comments?limit=50", owner, repo, index)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return nil, fmt.Errorf("fetching comments: %w", err)
}
defer resp.Body.Close()
var comments []Comment
if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil {
return nil, fmt.Errorf("decoding comments: %w", err)
}
// Populate convenience fields.
for i := range comments {
comments[i].User = comments[i].RawUser.Login
comments[i].CreatedAt = comments[i].RawCreatedAt.Format("Jan 2, 2006 15:04")
}
return comments, nil
}
// GetRepoLabels fetches all labels for a repository.
func (c *Client) GetRepoLabels(ctx context.Context, token, owner, repo string) ([]Label, error) {
path := fmt.Sprintf("/repos/%s/%s/labels?limit=50", owner, repo)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return nil, fmt.Errorf("fetching labels: %w", err)
}
defer resp.Body.Close()
var labels []Label
if err := json.NewDecoder(resp.Body).Decode(&labels); err != nil {
return nil, fmt.Errorf("decoding labels: %w", err)
}
return labels, nil
}
// CreateIssue creates a new issue in the specified repository. // CreateIssue creates a new issue in the specified repository.
func (c *Client) CreateIssue(ctx context.Context, token, owner, repo, title, body string, labels []int64) (*Issue, error) { func (c *Client) CreateIssue(ctx context.Context, token, owner, repo, title, body string, labels []int64) (*Issue, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
+216 -104
View File
@@ -7,6 +7,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config" "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config"
giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea" 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", h.CreateIssue)
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels) mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
// Issue detail.
mux.HandleFunc("GET /issues/{owner}/{repo}/{index}", h.IssueDetail)
// Pull requests. // Pull requests.
mux.HandleFunc("GET /pulls", h.ListPulls) 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) mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
// Settings (handled separately for auth bypass). // Settings (handled separately for auth bypass).
@@ -104,7 +109,7 @@ var basePage = template.Must(template.New("base").Parse(`<!DOCTYPE html>
<link rel="apple-touch-icon" href="/static/icon-192.png"> <link rel="apple-touch-icon" href="/static/icon-192.png">
<title>{{.Title}} — Gitea Mobile</title> <title>{{.Title}} — Gitea Mobile</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script> <script src="/static/htmx.min.js"></script>
</head> </head>
<body> <body>
<div class="content" id="main-content"> <div class="content" id="main-content">
@@ -175,158 +180,265 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgs := h.getUserOrgs(r)
if len(orgs) == 0 { type dashboardData struct {
renderPage(w, r, "Dashboard", "dashboard", Items []giteaclient.TriageItem
`<h1>Dashboard</h1><p class="empty">No organizations found. Check your token permissions.</p>`) Error string
return
} }
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) queue, err := h.Client.GetTriageQueue(r.Context(), token, orgs)
if err != nil { if err != nil {
slog.Error("failed to get triage queue", "error", err) slog.Error("failed to get triage queue", "error", err)
renderPage(w, r, "Dashboard", "dashboard", data.Error = "Error loading triage queue."
`<h1>Dashboard</h1><p class="empty">Error loading triage queue.</p>`) } 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 return
} }
if len(queue) == 0 { var buf strings.Builder
renderPage(w, r, "Dashboard", "dashboard", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Dashboard</h1><p class="empty">No items need attention. Nice work!</p>`) slog.Error("failed to execute dashboard template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Dashboard</h1>` renderPage(w, r, "Dashboard", "dashboard", buf.String())
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)
} }
// ListIssues handles GET /issues. // ListIssues handles GET /issues.
func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgNames := h.getUserOrgs(r)
if len(orgs) == 0 { type issuesData struct {
renderPage(w, r, "Issues", "issues", Issues []giteaclient.Issue
`<h1>Issues</h1><p class="empty">No organizations found.</p>`) Orgs []string
return 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 { if err != nil {
slog.Error("failed to list issues", "error", err) slog.Error("failed to list issues", "error", err)
renderPage(w, r, "Issues", "issues", data.Error = "Error loading issues."
`<h1>Issues</h1><p class="empty">Error loading issues.</p>`) } 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 return
} }
if len(issues) == 0 { var buf strings.Builder
renderPage(w, r, "Issues", "issues", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Issues</h1><p class="empty">No open issues found.</p>`) slog.Error("failed to execute issues template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Issues</h1>` renderPage(w, r, "Issues", "issues", buf.String())
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)
} }
// ListPulls handles GET /pulls. // ListPulls handles GET /pulls.
func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) { func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgNames := h.getUserOrgs(r)
if len(orgs) == 0 { type pullsData struct {
renderPage(w, r, "Pull Requests", "pulls", Pulls []giteaclient.PullRequest
`<h1>Pull Requests</h1><p class="empty">No organizations found.</p>`) Orgs []string
return 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 { if err != nil {
slog.Error("failed to list pull requests", "error", err) slog.Error("failed to list pull requests", "error", err)
renderPage(w, r, "Pull Requests", "pulls", data.Error = "Error loading pull requests."
`<h1>Pull Requests</h1><p class="empty">Error loading pull requests.</p>`) } 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 return
} }
if len(prs) == 0 { var buf strings.Builder
renderPage(w, r, "Pull Requests", "pulls", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Pull Requests</h1><p class="empty">No open pull requests found.</p>`) slog.Error("failed to execute pulls template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Pull Requests</h1>` renderPage(w, r, "Pull Requests", "pulls", buf.String())
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) // IssueDetail handles GET /issues/{owner}/{repo}/{index}.
mergeStatus := "" func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
if pr.Mergeable { token := getToken(r)
mergeStatus = `<span style="color:#3fb950;font-size:0.7rem;">mergeable</span>` 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
} }
content += fmt.Sprintf(`<div class="card"> issue, err := h.Client.GetIssue(r.Context(), token, owner, repo, index)
<div class="card-title"><span class="type-badge type-pull">PR</span> %s</div> if err != nil {
<div class="card-meta">%s/%s #%d %s %s %s</div> slog.Error("failed to get issue", "error", err, "owner", owner, "repo", repo, "index", index)
</div>`, template.HTMLEscapeString(pr.Title), renderPage(w, r, "Issue Not Found", "issues",
template.HTMLEscapeString(pr.RepoOwner), `<h1>Issue Not Found</h1><p class="empty">Could not load the requested issue.</p>`)
template.HTMLEscapeString(pr.RepoName), return
pr.Number, labels, stats, mergeStatus)
} }
renderPage(w, r, "Pull Requests", "pulls", content) 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. // CreateIssue handles POST /issues.
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="apple-touch-icon" href="/static/icon-192.png"> <link rel="apple-touch-icon" href="/static/icon-192.png">
<title>{{.Title}} — Gitea Mobile</title> <title>{{.Title}} — Gitea Mobile</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script> <script src="/static/htmx.min.js"></script>
</head> </head>
<body> <body>
<div class="content" id="main-content"> <div class="content" id="main-content">
+1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,14 +1,14 @@
// Service Worker for Gitea Mobile PWA // Service Worker for Gitea Mobile PWA
// Caches the app shell for offline/fast loading. // Caches the app shell for offline/fast loading.
const CACHE_NAME = 'gitea-mobile-v1'; const CACHE_NAME = 'gitea-mobile-v2';
const APP_SHELL = [ const APP_SHELL = [
'/', '/',
'/static/style.css', '/static/style.css',
'/static/manifest.json', '/static/manifest.json',
'/static/icon-192.png', '/static/icon-192.png',
'/static/icon-512.png', '/static/icon-512.png',
'https://unpkg.com/htmx.org@1.9.10' '/static/htmx.min.js'
]; ];
// Install: cache app shell resources. // Install: cache app shell resources.