# 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 ```html
Loading...
``` ### 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: ```css /* 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: ```dockerfile # 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 |