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 |
|
| `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 |
|
| `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
|
## API Endpoints
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ func main() {
|
||||||
adminAPI.HandleFunc("/admin/api/reports", handleAdminReports(db))
|
adminAPI.HandleFunc("/admin/api/reports", handleAdminReports(db))
|
||||||
adminAPI.HandleFunc("/admin/api/reports/status", handleAdminUpdateReport(db))
|
adminAPI.HandleFunc("/admin/api/reports/status", handleAdminUpdateReport(db))
|
||||||
adminAPI.HandleFunc("/admin/api/reports/delete", handleAdminDeleteReport(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))
|
mux.Handle("/admin/api/", adminAuth(adminToken, adminAPI))
|
||||||
log.Println("Admin interface enabled at /admin/")
|
log.Println("Admin interface enabled at /admin/")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,23 @@ func newNtfyNotifierFromEnv() *ntfyNotifier {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ntfyNotifier{
|
n := &ntfyNotifier{
|
||||||
url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"),
|
url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"),
|
||||||
user: strings.TrimSpace(os.Getenv("NTFY_USER")),
|
user: strings.TrimSpace(os.Getenv("NTFY_USER")),
|
||||||
token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")),
|
token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")),
|
||||||
clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"),
|
clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"),
|
||||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
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
|
// 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 {
|
func (n *ntfyNotifier) send(rep UserReport) error {
|
||||||
body := reportTypeLabel(rep.ReportType)
|
body := reportTypeLabel(rep.ReportType)
|
||||||
if rep.Message != "" {
|
if rep.Message != "" {
|
||||||
body += "\n\n" + rep.Message
|
body += "\n\n" + rep.Message
|
||||||
}
|
}
|
||||||
body += fmt.Sprintf("\n\nRepeater ID: %d", rep.RepeaterID)
|
if rep.RepeaterID > 0 {
|
||||||
|
body += fmt.Sprintf("\n\nRepeater ID: %d", rep.RepeaterID)
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, n.url, strings.NewReader(body))
|
req, err := http.NewRequest(http.MethodPost, n.url, strings.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -71,7 +93,7 @@ func (n *ntfyNotifier) send(rep UserReport) error {
|
||||||
req.Header.Set("Title", title)
|
req.Header.Set("Title", title)
|
||||||
req.Header.Set("Tags", "warning,radio")
|
req.Header.Set("Tags", "warning,radio")
|
||||||
req.Header.Set("Priority", "default")
|
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))
|
req.Header.Set("Click", fmt.Sprintf("%s/admin/#%d", n.clickBase, rep.RepeaterID))
|
||||||
}
|
}
|
||||||
n.setAuth(req)
|
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 {
|
func handleAdminDeleteReport(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodDelete {
|
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 active" data-report-filter="open">Open</button>
|
||||||
<button class="reports-filter-btn" data-report-filter="all">All</button>
|
<button class="reports-filter-btn" data-report-filter="all">All</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="report-action-btn" id="reports-test-btn">Test push</button>
|
||||||
<button class="history-modal-close" id="reports-close">×</button>
|
<button class="history-modal-close" id="reports-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-modal-body" id="reports-body"></div>
|
<div class="history-modal-body" id="reports-body"></div>
|
||||||
|
|
@ -952,6 +953,7 @@
|
||||||
var reportsModal = document.getElementById("reports-modal");
|
var reportsModal = document.getElementById("reports-modal");
|
||||||
var reportsBody = document.getElementById("reports-body");
|
var reportsBody = document.getElementById("reports-body");
|
||||||
var reportsClose = document.getElementById("reports-close");
|
var reportsClose = document.getElementById("reports-close");
|
||||||
|
var reportsTestBtn = document.getElementById("reports-test-btn");
|
||||||
var reportsFilter = "open";
|
var reportsFilter = "open";
|
||||||
|
|
||||||
function escapeHtml(s) {
|
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);
|
reportsBtn.addEventListener("click", showReports);
|
||||||
reportsClose.addEventListener("click", closeReports);
|
reportsClose.addEventListener("click", closeReports);
|
||||||
reportsOverlay.addEventListener("click", closeReports);
|
reportsOverlay.addEventListener("click", closeReports);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue