Files
0xWheatyz e93bb7e01c docs: add project roadmap for gitea mobile PWA
Covers three phases: Go backend with Gitea SDK aggregation layer,
HTMX mobile-first frontend with PWA support, and Docker/Kubernetes
deployment to Talos via FluxCD.
2026-03-24 22:25:13 -04:00

12 KiB

Gitea Mobile — Roadmap

A mobile-first PWA wrapping the Gitea API for managing issues and pull requests across multiple repos and organizations from an iPhone.

Tech Stack

Layer Choice Why
Backend Go + Gitea SDK (code.gitea.io/sdk/gitea) Native SDK covers every API surface, single static binary, tiny image
Frontend HTMX + Go html/template + hand-rolled CSS Zero client-side state, no build step, no node_modules, ~14KB JS
Container Multi-stage Dockerfile → distroless (~15MB) Minimal attack surface, fast pull
Deployment Kustomize manifests + FluxCD GitOps Matches existing Talos cluster patterns

Project Structure

/
├── cmd/server/main.go          # entrypoint
├── internal/
│   ├── config/config.go        # env-based configuration
│   ├── gitea/client.go         # Gitea SDK wrapper / aggregation layer
│   ├── handlers/               # HTTP handlers
│   │   ├── issues.go
│   │   ├── pulls.go
│   │   ├── triage.go
│   │   └── auth.go
│   ├── middleware/              # auth middleware, logging
│   └── templates/              # Go html/template files (for HTMX)
├── static/                     # CSS, JS (htmx.min.js), icons, manifest
├── Dockerfile
├── flake.nix
└── go.mod

Phase 1: Backend

1.1 — Project Scaffolding

  • Initialize Go module: go mod init gitea.leeworks.dev/0xwheatyz/gitea-mobile
  • Create flake.nix with Go 1.22+ toolchain and air for live reload
  • Set up directory structure per the project layout above

1.2 — Configuration

All config via environment variables (12-factor):

Variable Purpose Default
GITEA_URL Base URL of the Gitea instance required
GITEA_TOKEN API token (or per-user via cookie) optional
LISTEN_ADDR Server listen address :8080
SESSION_SECRET HMAC key for signing session cookies required

1.3 — Authentication

v1: Token-in-cookie (simple, recommended start)

  • User enters their Gitea API token once on a settings page
  • Token stored in a signed, encrypted HTTP-only cookie
  • All API calls use the user's token — respects Gitea's permission model
  • Cookies set with HttpOnly, Secure, SameSite=Strict

v2 (future): Authentik SSO integration

  • Traefik IngressRoute already supports Authentik middleware
  • Map Authentik identity to a stored Gitea token
  • Consistent with other cluster apps

1.4 — Gitea Aggregation Layer

internal/gitea/client.go — core aggregation functions the raw API doesn't provide:

Function Purpose
ListAllIssues(orgs []string) Fan-out across repos with errgroup, merge + sort by updated time
ListAllPullRequests(orgs []string) Same pattern for PRs, includes review status
GetTriageQueue() Unassigned issues + PRs awaiting review, sorted by priority labels
ListOrgsAndRepos() Enumerate all orgs the user belongs to, list repos per org
ApplyLabel() Thin wrapper for labeling issues
SubmitReview() Approve / request changes / comment on a PR
CreateIssue() Create an issue with labels in a specified repo

Design decisions:

  • Concurrent API calls across repos using errgroup with a semaphore (cap at 5-10)
  • In-memory cache with 30-second TTL using sync.RWMutex
  • Cache keys: orgs-repos, issues-{org}, pulls-{org}
  • Invalidate cache on write operations

1.5 — HTTP Handlers

Using Go 1.22+ stdlib http.ServeMux (no external router needed):

Route Method Purpose
/ GET Dashboard / triage queue
/issues GET All issues across orgs
/pulls GET All PRs across orgs
/issues/{owner}/{repo}/{index} GET Issue detail
/pulls/{owner}/{repo}/{index} GET PR detail + diff stats
/issues POST Create issue
/issues/{owner}/{repo}/{index}/labels POST Assign labels
/pulls/{owner}/{repo}/{index}/review POST Submit PR review
/settings GET Token configuration page
/settings POST Save token
/health GET K8s liveness/readiness probe

Each handler checks the HX-Request header to decide whether to return a full page or an HTMX HTML fragment.


Phase 2: Frontend (Mobile-First PWA)

2.1 — Layout

  • Fixed bottom navigation bar (4 tabs): Dashboard, Issues, PRs, Settings
  • Top bar with org/repo filter dropdown
  • Pull-to-refresh via HTMX polling or manual trigger
  • Dark mode via prefers-color-scheme media query
  • iPhone safe areas via env(safe-area-inset-bottom)

2.2 — Views

Dashboard / Triage (/)

  • Card-based list of items needing attention
  • Each card: repo name, issue/PR title, labels (colored badges), age
  • Tap to expand inline via HTMX hx-get loading a detail fragment

Issues List (/issues)

  • Filter bar: org, repo, state (open/closed), label
  • Infinite scroll via HTMX hx-trigger="revealed" on a sentinel element
  • Each row: colored dot for state, title, repo badge, label pills, assignee avatar

PR List (/pulls)

  • Same layout as issues plus: review status icon, merge status indicator
  • Tap to view PR detail with diff stats

Issue Detail (/issues/{owner}/{repo}/{index})

  • Title, body (rendered markdown via Gitea's API markdown endpoint)
  • Comments thread
  • Action buttons: Add label (dropdown), Assign, Comment, Close
  • All actions via HTMX hx-post with hx-swap="outerHTML"

PR Detail (/pulls/{owner}/{repo}/{index})

  • Same as issue detail plus: diff stat summary, file list, review form
  • Review form: textarea + radio (approve/request changes/comment) + submit
  • Mergeable status indicator

Create Issue (/issues/new)

  • Form: searchable repo selector, title, body textarea, label multi-select
  • Submit via HTMX, redirect to new issue detail

2.3 — HTMX Patterns

<!-- Infinite scroll -->
<div hx-get="/issues?page=1" hx-trigger="load" hx-swap="innerHTML">
  Loading...
</div>

<!-- Filter without full reload -->
<select hx-get="/issues" hx-trigger="change" hx-target="#issue-list"
        hx-include="[name='org']" name="org">
  <option value="all">All orgs</option>
</select>

<!-- Quick label assignment -->
<form hx-post="/issues/org/repo/42/labels" hx-swap="outerHTML"
      hx-target="closest .issue-card">
  <select name="label_id">...</select>
  <button type="submit">Apply</button>
</form>

2.4 — PWA Setup

Files in /static/:

  • manifest.json — app name "Gitea Mobile", display standalone, theme color, icons
  • sw.js — service worker caching the app shell (layout, CSS, HTMX JS, icons)
  • icon-192.png, icon-512.png — app icons
  • Apple-specific meta tags: apple-mobile-web-app-capable, apple-mobile-web-app-status-bar-style, apple-touch-icon

2.5 — CSS Strategy

Mobile-first with progressive enhancement, ~5KB target:

/* Base: mobile (< 640px) */
:root {
  --spacing: 0.5rem;
  --radius: 8px;
}

.card { margin: var(--spacing); padding: var(--spacing); }
.bottom-nav { position: fixed; bottom: 0; }

/* Safe area for iPhone notch */
.bottom-nav { padding-bottom: env(safe-area-inset-bottom); }

/* Tablet (>= 640px): 2-column grid */
@media (min-width: 640px) { ... }

Phase 3: Containerization & Talos Deployment

3.1 — Dockerfile

Multi-stage build producing a ~15-20MB image:

# 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 go build -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
ENTRYPOINT ["/gitea-mobile"]

3.2 — Container Registry

Push to Gitea container registry: gitea.leeworks.dev/0xwheatyz/gitea-mobile

Tag with timestamp + commit SHA. Flux image automation picks up new tags automatically via $imagepolicy annotations.

3.3 — Kubernetes Manifests

Create in the Talos repo at apps/gitea-mobile/:

File Purpose
namespace.yaml gitea-mobile namespace
deployment.yaml Single-container, health probes, resource limits
service.yaml ClusterIP on port 8080
secret.yaml SESSION_SECRET (migrate to sealed-secrets later)
ingressroute.yaml Traefik route at gitea-mobile.testing.leeworks.dev
kustomization.yaml Kustomize manifest listing all resources

Deployment details:

  • Resources: requests 64Mi / 50m, limits 256Mi / 500m
  • Probes: liveness + readiness on GET /health:8080
  • Strategy: Recreate (single replica)
  • Env: GITEA_URL=http://gitea.gitea.svc.cluster.local:3000, SESSION_SECRET from secret
  • IngressRoute: Authentik middleware, security-headers, TLS via wildcard-testing-leeworks-dev

3.4 — CI (Optional)

Gitea Actions workflow (.gitea/workflows/build.yaml):

  1. On push to main: run go test ./...
  2. Build Docker image, tag with timestamp + commit SHA
  3. Push to gitea.leeworks.dev registry
  4. Flux picks up new image tag automatically

Implementation Order

Step What Phase
1 Scaffold Go project + nix shell P1
2 Gitea SDK client + org/repo enumeration P1
3 Aggregation layer (issues, PRs, triage) P1
4 Auth (token-in-cookie) P1
5 HTTP handlers P1
6 Templates + HTMX + base layout P2
7 All views (dashboard, lists, detail, create) P2
8 Mobile CSS + dark mode P2
9 PWA manifest + service worker P2
10 Dockerfile + local test P3
11 K8s manifests in Talos repo P3
12 Push image + deploy + verify on phone P3

Risks & Mitigations

Risk Mitigation
Gitea API rate limiting with many repos In-memory cache (30s TTL), concurrent fetches with semaphore, lazy pagination
iPhone Safari PWA quirks Test early with iOS meta tags, standalone display, safe-area-inset handling
Token security in cookies HMAC-signed cookies, HttpOnly + Secure + SameSite=Strict
Secrets in Git Plaintext for v1, migrate to sealed-secrets per Talos roadmap
No hot-reload in dev air (Go live-reload) in the nix dev shell