e93bb7e01c
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.
308 lines
12 KiB
Markdown
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 |
|