• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DigitalTolk / ex / 25138047167

29 Apr 2026 10:53PM UTC coverage: 88.722% (-0.1%) from 88.844%
25138047167

push

github

web-flow
Add search (#36)

* Add search

* fix jump

* fix

* fix

* fixes

* aws

2577 of 3138 branches covered (82.12%)

Branch coverage included in aggregate %.

1645 of 1866 new or added lines in 44 files covered. (88.16%)

2 existing lines in 2 files now uncovered.

10765 of 11900 relevant lines covered (90.46%)

28.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.38
/internal/handler/admin.go
1
package handler
2

3
import (
4
        "context"
5
        "net/http"
6
        "time"
7

8
        "github.com/DigitalTolk/ex/internal/middleware"
9
        "github.com/DigitalTolk/ex/internal/model"
10
        "github.com/DigitalTolk/ex/internal/search"
11
        "github.com/DigitalTolk/ex/internal/service"
12
)
13

14
// SearchStatusReporter is the slim view AdminHandler needs to render
15
// the search status panel: cluster health + per-index docs/size. The
16
// concrete *search.Client satisfies this directly.
17
type SearchStatusReporter interface {
18
        ClusterHealth(ctx context.Context) (map[string]any, error)
19
        IndexStats(ctx context.Context) ([]search.IndexStat, error)
20
}
21

22
// AdminHandler exposes admin-only endpoints for workspace configuration.
23
// Authorization is enforced inside each handler — `middleware.Auth` only
24
// confirms the caller is signed in, not that they're an admin.
25
type AdminHandler struct {
26
        settings  *service.SettingsService
27
        searchSt  SearchStatusReporter
28
        reindexer *search.Reindexer
29
}
30

31
// NewAdminHandler constructs an AdminHandler.
32
func NewAdminHandler(settings *service.SettingsService) *AdminHandler {
17✔
33
        return &AdminHandler{settings: settings}
17✔
34
}
17✔
35

36
// SetSearch wires the optional search status reporter + reindexer.
37
// Production passes the live ones; tests can pass nil and the
38
// search-admin routes return 503.
39
func (h *AdminHandler) SetSearch(reporter SearchStatusReporter, reindexer *search.Reindexer) {
4✔
40
        h.searchSt = reporter
4✔
41
        h.reindexer = reindexer
4✔
42
}
4✔
43

44
// GetSettings returns the effective workspace settings (with defaults
45
// applied for any field the admin hasn't overridden). Available to all
46
// authenticated users so the upload UI can show the limits before
47
// attempting a request — the write side is admin-only.
48
func (h *AdminHandler) GetSettings(w http.ResponseWriter, r *http.Request) {
2✔
49
        if middleware.UserIDFromContext(r.Context()) == "" {
3✔
50
                writeError(w, http.StatusUnauthorized, "unauthorized", "authentication required")
1✔
51
                return
1✔
52
        }
1✔
53
        ws := h.settings.Effective(r.Context())
1✔
54
        writeJSON(w, http.StatusOK, ws)
1✔
55
}
56

57
// SearchStatus returns the search cluster's health, per-index doc
58
// counts/sizes, and the most recent reindex progress. Admin-only.
59
// Returns 503 with a structured payload (`configured: false`) when
60
// search isn't wired so the UI can render the panel without erroring.
61
func (h *AdminHandler) SearchStatus(w http.ResponseWriter, r *http.Request) {
4✔
62
        if !requireAdmin(w, r) {
5✔
63
                return
1✔
64
        }
1✔
65
        if h.searchSt == nil || h.reindexer == nil {
4✔
66
                writeJSON(w, http.StatusOK, JSON{"configured": false})
1✔
67
                return
1✔
68
        }
1✔
69
        ctx := r.Context()
2✔
70
        resp := JSON{"configured": true}
2✔
71
        if health, err := h.searchSt.ClusterHealth(ctx); err == nil && health != nil {
3✔
72
                resp["cluster"] = health
1✔
73
        } else if err != nil {
3✔
74
                resp["clusterError"] = err.Error()
1✔
75
        }
1✔
76
        if stats, err := h.searchSt.IndexStats(ctx); err == nil {
3✔
77
                resp["indices"] = stats
1✔
78
        } else {
2✔
79
                resp["indicesError"] = err.Error()
1✔
80
        }
1✔
81
        resp["reindex"] = h.reindexer.Status()
2✔
82
        writeJSON(w, http.StatusOK, resp)
2✔
83
}
84

85
// StartSearchReindex kicks off a full rebuild in the background.
86
// Admin-only. Returns 202 if a fresh run started, 409 if one is
87
// already running, 503 if search isn't configured.
88
func (h *AdminHandler) StartSearchReindex(w http.ResponseWriter, r *http.Request) {
3✔
89
        if !requireAdmin(w, r) {
4✔
90
                return
1✔
91
        }
1✔
92
        if h.reindexer == nil {
3✔
93
                writeError(w, http.StatusServiceUnavailable, "search_disabled", "search is not configured")
1✔
94
                return
1✔
95
        }
1✔
96
        // Detach the request context so the goroutine survives the HTTP
97
        // response. Reindexes routinely outlive the request timeout —
98
        // admins poll status via SearchStatus afterwards.
99
        if !h.reindexer.Start(context.Background(), nowUnix) {
1✔
NEW
100
                writeError(w, http.StatusConflict, "already_running", "a reindex is already running")
×
NEW
101
                return
×
NEW
102
        }
×
103
        writeJSON(w, http.StatusAccepted, h.reindexer.Status())
1✔
104
}
105

106
func nowUnix() int64 { return time.Now().Unix() }
3✔
107

108
// UpdateSettings replaces the workspace settings. Admin-only.
109
func (h *AdminHandler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
4✔
110
        if !requireAdmin(w, r) {
5✔
111
                return
1✔
112
        }
1✔
113
        var body model.WorkspaceSettings
3✔
114
        if err := readJSON(r, &body); err != nil {
4✔
115
                writeError(w, http.StatusBadRequest, "invalid_body", err.Error())
1✔
116
                return
1✔
117
        }
1✔
118
        out, err := h.settings.Update(r.Context(), &body)
2✔
119
        if err != nil {
3✔
120
                writeError(w, http.StatusBadRequest, "update_error", err.Error())
1✔
121
                return
1✔
122
        }
1✔
123
        writeJSON(w, http.StatusOK, out)
1✔
124
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc