Add test push button in admin area
This commit is contained in:
parent
14550b3797
commit
a7ab445281
5 changed files with 85 additions and 4 deletions
|
|
@ -83,7 +83,7 @@ docker compose -f compose.test.yml run --rm test
|
|||
| `NTFY_TOKEN` | *(none)* | ntfy access token. Sent as `Authorization: Bearer` when no username is set |
|
||||
| `PUBLIC_BASE_URL` | *(none)* | Public base URL of the site (e.g. `https://dmrmap.de`). Used to add a click-through link to the admin view in ntfy notifications |
|
||||
|
||||
Both `NTFY_URL` and `NTFY_TOPIC` must be set for notifications; otherwise reports are stored but nothing is pushed.
|
||||
Both `NTFY_URL` and `NTFY_TOPIC` must be set for notifications; otherwise reports are stored but nothing is pushed. On startup the app logs either `ntfy notifications enabled: <topic URL> (auth: ...)` or `ntfy notifications disabled`. The **Test push** button in the admin Reports view publishes a test notification and shows the upstream error if it fails.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ func main() {
|
|||
adminAPI.HandleFunc("/admin/api/reports", handleAdminReports(db))
|
||||
adminAPI.HandleFunc("/admin/api/reports/status", handleAdminUpdateReport(db))
|
||||
adminAPI.HandleFunc("/admin/api/reports/delete", handleAdminDeleteReport(db))
|
||||
adminAPI.HandleFunc("/admin/api/reports/test-notification", handleAdminTestNotification(notifier))
|
||||
mux.Handle("/admin/api/", adminAuth(adminToken, adminAPI))
|
||||
log.Println("Admin interface enabled at /admin/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,23 @@ func newNtfyNotifierFromEnv() *ntfyNotifier {
|
|||
return nil
|
||||
}
|
||||
|
||||
return &ntfyNotifier{
|
||||
n := &ntfyNotifier{
|
||||
url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"),
|
||||
user: strings.TrimSpace(os.Getenv("NTFY_USER")),
|
||||
token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")),
|
||||
clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"),
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
auth := "none"
|
||||
if n.user != "" && n.token != "" {
|
||||
auth = "basic (" + n.user + ")"
|
||||
} else if n.token != "" {
|
||||
auth = "bearer token"
|
||||
}
|
||||
log.Printf("ntfy notifications enabled: %s (auth: %s)", n.url, auth)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// notifyReport sends a push notification for a new user report. It never
|
||||
|
|
@ -52,12 +62,24 @@ func (n *ntfyNotifier) notifyReport(rep UserReport) {
|
|||
}()
|
||||
}
|
||||
|
||||
// sendTest publishes a test notification and reports the outcome synchronously,
|
||||
// so the admin UI can surface configuration problems.
|
||||
func (n *ntfyNotifier) sendTest() error {
|
||||
return n.send(UserReport{
|
||||
Callsign: "TEST",
|
||||
ReportType: "other",
|
||||
Message: "Test notification from DMRmap admin.",
|
||||
})
|
||||
}
|
||||
|
||||
func (n *ntfyNotifier) send(rep UserReport) error {
|
||||
body := reportTypeLabel(rep.ReportType)
|
||||
if rep.Message != "" {
|
||||
body += "\n\n" + rep.Message
|
||||
}
|
||||
if rep.RepeaterID > 0 {
|
||||
body += fmt.Sprintf("\n\nRepeater ID: %d", rep.RepeaterID)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, n.url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
|
|
@ -71,7 +93,7 @@ func (n *ntfyNotifier) send(rep UserReport) error {
|
|||
req.Header.Set("Title", title)
|
||||
req.Header.Set("Tags", "warning,radio")
|
||||
req.Header.Set("Priority", "default")
|
||||
if n.clickBase != "" {
|
||||
if n.clickBase != "" && rep.RepeaterID > 0 {
|
||||
req.Header.Set("Click", fmt.Sprintf("%s/admin/#%d", n.clickBase, rep.RepeaterID))
|
||||
}
|
||||
n.setAuth(req)
|
||||
|
|
|
|||
|
|
@ -246,6 +246,38 @@ func handleAdminUpdateReport(db *sql.DB) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// handleAdminTestNotification publishes a test push so ntfy credentials can be
|
||||
// verified without filing a report. Unlike the report path it waits for the
|
||||
// result and returns the upstream error verbatim.
|
||||
func handleAdminTestNotification(notifier *ntfyNotifier) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if notifier == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(`{"error":"ntfy is not configured (NTFY_URL and NTFY_TOPIC must be set)"}`))
|
||||
return
|
||||
}
|
||||
|
||||
if err := notifier.sendTest(); err != nil {
|
||||
log.Printf("ntfy: test notification failed: %v", err)
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": err.Error(),
|
||||
"url": notifier.url,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "url": notifier.url})
|
||||
}
|
||||
}
|
||||
|
||||
func handleAdminDeleteReport(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
|
|
|
|||
|
|
@ -881,6 +881,7 @@
|
|||
<button class="reports-filter-btn active" data-report-filter="open">Open</button>
|
||||
<button class="reports-filter-btn" data-report-filter="all">All</button>
|
||||
</div>
|
||||
<button class="report-action-btn" id="reports-test-btn">Test push</button>
|
||||
<button class="history-modal-close" id="reports-close">×</button>
|
||||
</div>
|
||||
<div class="history-modal-body" id="reports-body"></div>
|
||||
|
|
@ -952,6 +953,7 @@
|
|||
var reportsModal = document.getElementById("reports-modal");
|
||||
var reportsBody = document.getElementById("reports-body");
|
||||
var reportsClose = document.getElementById("reports-close");
|
||||
var reportsTestBtn = document.getElementById("reports-test-btn");
|
||||
var reportsFilter = "open";
|
||||
|
||||
function escapeHtml(s) {
|
||||
|
|
@ -1857,6 +1859,30 @@
|
|||
});
|
||||
});
|
||||
|
||||
reportsTestBtn.addEventListener("click", function () {
|
||||
reportsTestBtn.disabled = true;
|
||||
// Not apiFetch: the error body carries the ntfy failure reason.
|
||||
fetch("/admin/api/reports/test-notification", {
|
||||
method: "POST",
|
||||
headers: { "Authorization": "Bearer " + token },
|
||||
})
|
||||
.then(function (resp) {
|
||||
return resp.json().then(function (data) { return { ok: resp.ok, data: data }; });
|
||||
})
|
||||
.then(function (r) {
|
||||
reportsTestBtn.disabled = false;
|
||||
if (r.ok) {
|
||||
alert("Test notification published to " + r.data.url);
|
||||
} else {
|
||||
alert("Test notification failed: " + (r.data.error || "unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
reportsTestBtn.disabled = false;
|
||||
alert("Test notification failed: " + err.message);
|
||||
});
|
||||
});
|
||||
|
||||
reportsBtn.addEventListener("click", showReports);
|
||||
reportsClose.addEventListener("click", closeReports);
|
||||
reportsOverlay.addEventListener("click", closeReports);
|
||||
|
|
|
|||
Loading…
Reference in a new issue