detached a01c57b

feat(api): add /api/config endpoint to serve search shortcuts

- Add handleAPI and handleConfigAPI methods to SchemeHandler
- Pass config to SchemeHandler constructor
- Fixes 404 error when frontend fetches search shortcuts
commit
a01c57b0853571cb5e1e2e613298cc9a18d21eff
author
bamen · · 2025-10-06 16:28 UTC
committer
bamen · · 2025-10-06 21:17 UTC
parent
2b84462
signature
unsigned
2 files · +42 -1
M internal/app/api/scheme.go +41 -0
@@ -2,6 +2,7 @@ package api
22
33 import (
44 "embed"
5+ "encoding/json"
56 "log"
67 "mime"
78 neturl "net/url"
@@ -18,6 +19,7 @@ type SchemeHandler struct {
1819 assets embed.FS
1920 parserService *services.ParserService
2021 browserService *services.BrowserService
22+ config *config.Config
2123 }
2224
2325 // NewSchemeHandler creates a new scheme handler
@@ -25,11 +27,13 @@ func NewSchemeHandler(
2527 assets embed.FS,
2628 parserService *services.ParserService,
2729 browserService *services.BrowserService,
30+ cfg *config.Config,
2831 ) *SchemeHandler {
2932 return &SchemeHandler{
3033 assets: assets,
3134 parserService: parserService,
3235 browserService: browserService,
36+ config: cfg,
3337 }
3438 }
3539
@@ -52,6 +56,11 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
5256
5357 // handleAsset serves static assets from embedded filesystem
5458 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
59+ // Check if this is an API route
60+ if strings.HasPrefix(u.Path, "/api/") {
61+ return s.handleAPI(u)
62+ }
63+
5564 // Resolve target path inside embed FS
5665 var rel string
5766 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -101,6 +110,38 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
101110 return mt, data, true
102111 }
103112
113+// handleAPI handles API routes
114+func (s *SchemeHandler) handleAPI(u *neturl.URL) (string, []byte, bool) {
115+ switch u.Path {
116+ case "/api/config":
117+ return s.handleConfigAPI()
118+ default:
119+ log.Printf("[scheme] API route not found: %s", u.Path)
120+ return "", nil, false
121+ }
122+}
123+
124+// handleConfigAPI returns the application configuration as JSON
125+func (s *SchemeHandler) handleConfigAPI() (string, []byte, bool) {
126+ if s.config == nil {
127+ log.Printf("[scheme] config is nil, cannot serve /api/config")
128+ return "", nil, false
129+ }
130+
131+ // Create a response with search shortcuts
132+ response := map[string]interface{}{
133+ "search_shortcuts": s.config.SearchShortcuts,
134+ }
135+
136+ data, err := json.Marshal(response)
137+ if err != nil {
138+ log.Printf("[scheme] failed to marshal config: %v", err)
139+ return "", nil, false
140+ }
141+
142+ return "application/json", data, true
143+}
144+
104145 // getMimeType determines the MIME type for a given file path
105146 func (s *SchemeHandler) getMimeType(filename string) string {
106147 ext := strings.ToLower(filepath.Ext(filename))
@@ -2,6 +2,7 @@ package api
2 2
3 import (3 import (
4 "embed"4 "embed"
5+ "encoding/json"
5 "log"6 "log"
6 "mime"7 "mime"
7 neturl "net/url"8 neturl "net/url"
@@ -18,6 +19,7 @@ type SchemeHandler struct {
18 assets embed.FS19 assets embed.FS
19 parserService *services.ParserService20 parserService *services.ParserService
20 browserService *services.BrowserService21 browserService *services.BrowserService
22+ config *config.Config
21 }23 }
22 24
23 // NewSchemeHandler creates a new scheme handler25 // NewSchemeHandler creates a new scheme handler
@@ -25,11 +27,13 @@ func NewSchemeHandler(
25 assets embed.FS,27 assets embed.FS,
26 parserService *services.ParserService,28 parserService *services.ParserService,
27 browserService *services.BrowserService,29 browserService *services.BrowserService,
30+ cfg *config.Config,
28 ) *SchemeHandler {31 ) *SchemeHandler {
29 return &SchemeHandler{32 return &SchemeHandler{
30 assets: assets,33 assets: assets,
31 parserService: parserService,34 parserService: parserService,
32 browserService: browserService,35 browserService: browserService,
36+ config: cfg,
33 }37 }
34 }38 }
35 39
@@ -52,6 +56,11 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
52 56
53 // handleAsset serves static assets from embedded filesystem57 // handleAsset serves static assets from embedded filesystem
54 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {58 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
59+ // Check if this is an API route
60+ if strings.HasPrefix(u.Path, "/api/") {
61+ return s.handleAPI(u)
62+ }
63+
55 // Resolve target path inside embed FS64 // Resolve target path inside embed FS
56 var rel string65 var rel string
57 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {66 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -101,6 +110,38 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
101 return mt, data, true110 return mt, data, true
102 }111 }
103 112
113+// handleAPI handles API routes
114+func (s *SchemeHandler) handleAPI(u *neturl.URL) (string, []byte, bool) {
115+ switch u.Path {
116+ case "/api/config":
117+ return s.handleConfigAPI()
118+ default:
119+ log.Printf("[scheme] API route not found: %s", u.Path)
120+ return "", nil, false
121+ }
122+}
123+
124+// handleConfigAPI returns the application configuration as JSON
125+func (s *SchemeHandler) handleConfigAPI() (string, []byte, bool) {
126+ if s.config == nil {
127+ log.Printf("[scheme] config is nil, cannot serve /api/config")
128+ return "", nil, false
129+ }
130+
131+ // Create a response with search shortcuts
132+ response := map[string]interface{}{
133+ "search_shortcuts": s.config.SearchShortcuts,
134+ }
135+
136+ data, err := json.Marshal(response)
137+ if err != nil {
138+ log.Printf("[scheme] failed to marshal config: %v", err)
139+ return "", nil, false
140+ }
141+
142+ return "application/json", data, true
143+}
144+
104 // getMimeType determines the MIME type for a given file path145 // getMimeType determines the MIME type for a given file path
105 func (s *SchemeHandler) getMimeType(filename string) string {146 func (s *SchemeHandler) getMimeType(filename string) string {
106 ext := strings.ToLower(filepath.Ext(filename))147 ext := strings.ToLower(filepath.Ext(filename))
M internal/app/browser/browser.go +1 -1
@@ -139,7 +139,7 @@ func (app *BrowserApp) Initialize() error {
139139 app.browserService = services.NewBrowserService(app.config, app.queries)
140140
141141 // Initialize handlers
142- app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)
142+ app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService, app.config)
143143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
144144
145145 return nil
@@ -139,7 +139,7 @@ func (app *BrowserApp) Initialize() error {
139 app.browserService = services.NewBrowserService(app.config, app.queries)139 app.browserService = services.NewBrowserService(app.config, app.queries)
140 140
141 // Initialize handlers141 // Initialize handlers
142- app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)142+ app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService, app.config)
143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
144 144
145 return nil145 return nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close