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

308 lines
12 KiB
Markdown

# 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
<!-- 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:
```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 |