From e93bb7e01cb4e658e9b6f8053633cd53fe6fa775 Mon Sep 17 00:00:00 2001 From: 0xWheatyz Date: Tue, 24 Mar 2026 22:25:13 -0400 Subject: [PATCH] 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. --- ROADMAP.md | 307 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..1cc15eb --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,307 @@ +# 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 |