chore: added public API to kepler dbf4ae9c
Steve Simkins · 2026-06-14 19:49 5 file(s) · +125 −5
apps/kepler/README.md +26 −0
15 15
- Branches + tags page
16 16
- Archive download: `.tar.gz`, `.zip`
17 17
- Atom feed per repo
18 +
- Public read-only JSON API
19 +
20 +
## API
21 +
22 +
`GET /api/repos` → JSON list of all repos (no auth, CORS `*`):
23 +
24 +
```json
25 +
{
26 +
  "site": "kepler",
27 +
  "repos": [
28 +
    {
29 +
      "name": "andromeda",
30 +
      "description": "monorepo",
31 +
      "default_ref": "main",
32 +
      "last_commit": "2026-06-14T10:00:00Z",
33 +
      "url": "https://git.example.com/andromeda",
34 +
      "atom_url": "https://git.example.com/andromeda/atom.xml",
35 +
      "clone_https": "https://git.example.com/andromeda.git",
36 +
      "clone_ssh": "git@git.example.com:andromeda.git"
37 +
    }
38 +
  ]
39 +
}
40 +
```
41 +
42 +
`url` / `atom_url` use the request host (honors `X-Forwarded-Proto`); clone
43 +
fields appear only when `KEPLER_CLONE_BASE_URL` / `KEPLER_CLONE_SSH_HOST` are set.
18 44
19 45
## Run
20 46
apps/kepler/api.go (added) +60 −0
1 +
package main
2 +
3 +
import (
4 +
	"encoding/json"
5 +
	"net/http"
6 +
	"time"
7 +
)
8 +
9 +
type apiRepo struct {
10 +
	Name        string    `json:"name"`
11 +
	Description string    `json:"description,omitempty"`
12 +
	DefaultRef  string    `json:"default_ref,omitempty"`
13 +
	LastCommit  time.Time `json:"last_commit,omitempty"`
14 +
	URL         string    `json:"url"`
15 +
	AtomURL     string    `json:"atom_url"`
16 +
	CloneHTTPS  string    `json:"clone_https,omitempty"`
17 +
	CloneSSH    string    `json:"clone_ssh,omitempty"`
18 +
}
19 +
20 +
type apiReposResponse struct {
21 +
	Site  string    `json:"site"`
22 +
	Repos []apiRepo `json:"repos"`
23 +
}
24 +
25 +
func (a *App) apiReposHandler(w http.ResponseWriter, r *http.Request) {
26 +
	repos, err := a.listRepos()
27 +
	if err != nil {
28 +
		a.Log.Error("list repos failed", "err", err)
29 +
		http.Error(w, "failed to list repos", http.StatusInternalServerError)
30 +
		return
31 +
	}
32 +
33 +
	baseURL := a.requestBaseURL(r)
34 +
	resp := apiReposResponse{Site: a.SiteName, Repos: make([]apiRepo, 0, len(repos))}
35 +
	for _, s := range repos {
36 +
		ar := apiRepo{
37 +
			Name:        s.Name,
38 +
			Description: s.Description,
39 +
			DefaultRef:  s.DefaultRef,
40 +
			LastCommit:  s.LastCommit,
41 +
			URL:         baseURL + "/" + s.Name,
42 +
			AtomURL:     baseURL + "/" + s.Name + "/atom.xml",
43 +
		}
44 +
		if a.CloneBaseURL != "" {
45 +
			ar.CloneHTTPS = a.CloneBaseURL + "/" + s.Name + ".git"
46 +
		}
47 +
		if a.CloneSSHHost != "" {
48 +
			ar.CloneSSH = a.CloneSSHHost + ":" + s.Name + ".git"
49 +
		}
50 +
		resp.Repos = append(resp.Repos, ar)
51 +
	}
52 +
53 +
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
54 +
	w.Header().Set("Access-Control-Allow-Origin", "*")
55 +
	enc := json.NewEncoder(w)
56 +
	enc.SetIndent("", "  ")
57 +
	if err := enc.Encode(resp); err != nil {
58 +
		a.Log.Error("api repos encode failed", "err", err)
59 +
	}
60 +
}
apps/kepler/handlers.go +11 −5
14 14
	return pageBase{SiteName: a.SiteName, RepoName: repoName, BaseURL: a.BaseURL}
15 15
}
16 16
17 +
// requestBaseURL derives the externally-visible base URL (scheme://host) from
18 +
// the incoming request, honoring a reverse proxy's X-Forwarded-Proto.
19 +
func (a *App) requestBaseURL(r *http.Request) string {
20 +
	scheme := "http"
21 +
	if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
22 +
		scheme = "https"
23 +
	}
24 +
	return scheme + "://" + r.Host
25 +
}
26 +
17 27
func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) {
18 28
	repos, err := a.listRepos()
19 29
	if err != nil {
268 278
		http.Error(w, "log failed", http.StatusInternalServerError)
269 279
		return
270 280
	}
271 -
	scheme := "http"
272 -
	if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
273 -
		scheme = "https"
274 -
	}
275 -
	baseURL := scheme + "://" + r.Host
281 +
	baseURL := a.requestBaseURL(r)
276 282
	feed, err := buildAtomFeed(a.SiteName, summary.Name, baseURL, commits)
277 283
	if err != nil {
278 284
		http.Error(w, "atom build failed", http.StatusInternalServerError)
apps/kepler/routes.go +2 −0
56 56
		mux.HandleFunc("GET /assets/"+name, staticAsset)
57 57
	}
58 58
59 +
	mux.HandleFunc("GET /api/repos", a.apiReposHandler)
60 +
59 61
	mux.HandleFunc("GET /{$}", a.indexHandler)
60 62
	mux.HandleFunc("GET /{repo}", a.repoHandler)
61 63
	mux.HandleFunc("GET /{repo}/refs", a.refsHandler)
docs/docs/pages/apps/kepler.mdx +26 −0
13 13
- Branches + tags page
14 14
- Archive download: `.tar.gz`, `.zip`
15 15
- Atom feed per repo
16 +
- Public read-only JSON API
16 17
- Dark themed UI with Commit Mono font
17 18
18 19
## Configure
87 88
| `/{repo}/raw/{ref}/{path...}` | Raw file download |
88 89
| `/{repo}/archive/{name}` | Archive download (`.tar.gz`, `.zip`) |
89 90
| `/{repo}/atom.xml` | Per-repo Atom feed |
91 +
| `/api/repos` | Public JSON list of all repos |
92 +
93 +
### API
94 +
95 +
`GET /api/repos` returns a JSON list of every repo. No auth, CORS open (`Access-Control-Allow-Origin: *`).
96 +
97 +
```json
98 +
{
99 +
  "site": "kepler",
100 +
  "repos": [
101 +
    {
102 +
      "name": "andromeda",
103 +
      "description": "monorepo",
104 +
      "default_ref": "main",
105 +
      "last_commit": "2026-06-14T10:00:00Z",
106 +
      "url": "https://git.example.com/andromeda",
107 +
      "atom_url": "https://git.example.com/andromeda/atom.xml",
108 +
      "clone_https": "https://git.example.com/andromeda.git",
109 +
      "clone_ssh": "git@git.example.com:andromeda.git"
110 +
    }
111 +
  ]
112 +
}
113 +
```
114 +
115 +
`url` and `atom_url` are derived from the request host (honors `X-Forwarded-Proto` behind a proxy). The `clone_https` / `clone_ssh` fields appear only when `KEPLER_CLONE_BASE_URL` / `KEPLER_CLONE_SSH_HOST` are set.