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.
This commit is contained in:
+307
@@ -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
|
||||
<!-- 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 |
|
||||
Reference in New Issue
Block a user