From c1ce81456da323d72fe8b88eff4944b01abef8c7 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 11 Feb 2026 09:52:06 +0100 Subject: [PATCH] Implement manually updating BM status --- admin.go | 54 ++++++++++++ main.go | 1 + static/admin.html | 203 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 254 insertions(+), 4 deletions(-) diff --git a/admin.go b/admin.go index e63c0ec..279bf81 100644 --- a/admin.go +++ b/admin.go @@ -179,6 +179,60 @@ func handleAdminRepeaters(db *sql.DB) http.HandlerFunc { } } +func handleAdminSaveBMData(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) + return + } + + var payload struct { + ID int `json:"id"` + LastSeen string `json:"last_seen"` + BmStatus *int `json:"bm_status"` + BmStatusText string `json:"bm_status_text"` + Hardware string `json:"hardware"` + Firmware string `json:"firmware"` + Pep int `json:"pep"` + Agl int `json:"agl"` + Website string `json:"website"` + Description string `json:"description"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest) + return + } + if payload.ID <= 0 { + http.Error(w, `{"error":"missing id"}`, http.StatusBadRequest) + return + } + + var lastSeen *time.Time + if payload.LastSeen != "" { + if t, err := time.Parse("2006-01-02 15:04:05", payload.LastSeen); err == nil { + lastSeen = &t + } + } + + _, err := db.Exec(`UPDATE repeaters SET + last_seen=$1, bm_status=$2, bm_status_text=$3, + hardware=$4, firmware=$5, pep=$6, agl=$7, + website=$8, description=$9, last_polled=NOW() + WHERE id=$10`, + lastSeen, payload.BmStatus, payload.BmStatusText, + payload.Hardware, payload.Firmware, payload.Pep, payload.Agl, + payload.Website, payload.Description, payload.ID) + if err != nil { + log.Printf("Admin save BM data: update failed for %d: %v", payload.ID, err) + http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) + } +} + func parseAdminFilters(q url.Values) ([]Filter, string) { mode := q.Get("filter_mode") if mode != "or" { diff --git a/main.go b/main.go index 94f174f..eaf2b24 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { adminAPI.HandleFunc("/admin/api/repeaters", handleAdminRepeaters(db)) adminAPI.HandleFunc("/admin/api/repeaters/update", handleAdminUpdateRepeater(db)) adminAPI.HandleFunc("/admin/api/repeaters/bm-device", handleBMDevice()) + adminAPI.HandleFunc("/admin/api/repeaters/save-bm", handleAdminSaveBMData(db)) mux.Handle("/admin/api/", adminAuth(adminToken, adminAPI)) log.Println("Admin interface enabled at /admin/") } diff --git a/static/admin.html b/static/admin.html index 9c0f71f..432ba30 100644 --- a/static/admin.html +++ b/static/admin.html @@ -535,6 +535,42 @@ cursor: pointer; } .filter-add-btn:hover { border-color: var(--text-muted); color: var(--text-muted); } + + .bm-review-bar { + background: var(--tag-blue-bg); + border-bottom: 1px solid var(--border-light); + padding: 10px 24px; + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + color: var(--tag-blue-text); + } + .bm-review-bar .bm-review-text { flex: 1; } + .bm-review-bar button { + padding: 6px 14px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + } + .btn-bm-save { + background: var(--accent); + color: white; + border: none; + } + .btn-bm-save:hover { background: var(--accent-hover); } + .btn-bm-save:disabled { opacity: 0.5; cursor: default; } + .btn-bm-discard { + background: none; + border: 1px solid var(--border); + color: var(--text-muted); + } + .btn-bm-discard:hover { background: var(--hover-bg); } + + tr.bm-changed td { background: var(--tag-blue-bg); } + tr.bm-changed:hover td { background: var(--tag-blue-bg); filter: brightness(0.95); } + tr.bm-error td { background: var(--tag-red-bg); } @@ -553,6 +589,7 @@

DMRmap Admin

+ @@ -564,6 +601,11 @@
+
@@ -645,6 +687,13 @@ var filterBar = document.getElementById("filter-bar"); var filterList = document.getElementById("filter-list"); var addFilterBtn = document.getElementById("add-filter-btn"); + var fetchAllBmBtn = document.getElementById("fetch-all-bm-btn"); + var bmReviewBar = document.getElementById("bm-review-bar"); + var bmReviewText = document.getElementById("bm-review-text"); + var bmSaveBtn = document.getElementById("bm-save-btn"); + var bmDiscardBtn = document.getElementById("bm-discard-btn"); + + var pendingBMData = {}; // keyed by repeater ID: { bmData, original } var filterDefs = { country: { label: "Country", ops: [{v:"eq",l:"is"},{v:"neq",l:"is not"},{v:"contains",l:"contains"}], hasValue: true }, @@ -807,7 +856,148 @@ }); }); + // --- Bulk BM fetch --- + + function fetchAllBMData() { + var bmRepeaters = currentRepeaters.filter(function (r) { + return r.networks && r.networks.indexOf("Brandmeister") !== -1; + }); + if (!bmRepeaters.length) { + totalInfo.textContent = "No BrandMeister repeaters on this page"; + return; + } + + pendingBMData = {}; + fetchAllBmBtn.disabled = true; + fetchAllBmBtn.textContent = "Fetching 0/" + bmRepeaters.length; + + var idx = 0; + var changed = 0; + var errors = 0; + var fetched = 0; + + function scheduleNext() { + // 5s pause after every 10 fetches, otherwise 500ms (2/sec max) + var delay = (fetched > 0 && fetched % 10 === 0) ? 5000 : 500; + if (fetched > 0 && fetched % 10 === 0) { + fetchAllBmBtn.textContent = "Pausing... " + idx + "/" + bmRepeaters.length; + } + setTimeout(next, delay); + } + + function next() { + if (idx >= bmRepeaters.length) { + fetchAllBmBtn.disabled = false; + fetchAllBmBtn.textContent = "Fetch All BM"; + if (changed > 0) { + bmReviewText.textContent = changed + " repeater" + (changed !== 1 ? "s" : "") + " with new BM data" + (errors > 0 ? " (" + errors + " failed)" : ""); + bmReviewBar.style.display = "flex"; + } else { + totalInfo.textContent = "No changes found" + (errors > 0 ? " (" + errors + " failed)" : ""); + } + renderTable(currentRepeaters); + return; + } + + var r = bmRepeaters[idx]; + fetchAllBmBtn.textContent = "Fetching " + (idx + 1) + "/" + bmRepeaters.length; + + apiFetch("/admin/api/repeaters/bm-device?id=" + r.id) + .then(function (bm) { + var hasChange = + (bm.status_text || "") !== (r.bm_status_text || "") || + bm.status !== r.bm_status || + (bm.hardware || "") !== (r.hardware || "") || + (bm.firmware || "") !== (r.firmware || "") || + bm.pep !== r.pep || + bm.agl !== r.agl || + (bm.website || "") !== (r.website || "") || + (bm.description || "") !== (r.description || ""); + + if (hasChange) { + pendingBMData[r.id] = bm; + changed++; + } + idx++; + fetched++; + scheduleNext(); + }) + .catch(function (err) { + if (err.message === "unauthorized") return; + errors++; + idx++; + fetched++; + scheduleNext(); + }); + } + + next(); + } + + function discardBMData() { + pendingBMData = {}; + bmReviewBar.style.display = "none"; + renderTable(currentRepeaters); + } + + function saveAllBMData() { + var ids = Object.keys(pendingBMData); + if (!ids.length) return; + + bmSaveBtn.disabled = true; + bmReviewText.textContent = "Saving 0/" + ids.length + "..."; + var idx = 0; + var saved = 0; + var errors = 0; + + function nextSave() { + if (idx >= ids.length) { + bmSaveBtn.disabled = false; + pendingBMData = {}; + bmReviewBar.style.display = "none"; + fetchRepeaters(); + return; + } + + var id = parseInt(ids[idx]); + var bm = pendingBMData[id]; + bmReviewText.textContent = "Saving " + (idx + 1) + "/" + ids.length + "..."; + + apiFetch("/admin/api/repeaters/save-bm", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: id, + last_seen: bm.last_seen || "", + bm_status: bm.status, + bm_status_text: bm.status_text || "", + hardware: bm.hardware || "", + firmware: bm.firmware || "", + pep: bm.pep || 0, + agl: bm.agl || 0, + website: bm.website || "", + description: bm.description || "" + }) + }) + .then(function () { saved++; idx++; nextSave(); }) + .catch(function (err) { + if (err.message === "unauthorized") return; + errors++; + idx++; + nextSave(); + }); + } + + nextSave(); + } + + fetchAllBmBtn.addEventListener("click", fetchAllBMData); + bmSaveBtn.addEventListener("click", saveAllBMData); + bmDiscardBtn.addEventListener("click", discardBMData); + function fetchRepeaters() { + pendingBMData = {}; + bmReviewBar.style.display = "none"; loadingEl.style.display = ""; emptyState.style.display = "none"; tableBody.innerHTML = ""; @@ -851,6 +1041,9 @@ repeaters.forEach(function (r, idx) { var tr = document.createElement("tr"); + var bm = pendingBMData[r.id]; + if (bm) tr.className = "bm-changed"; + var bandTag = r.band === "2m" ? '2m' : '70cm'; @@ -859,7 +1052,9 @@ ? 'Inactive' : '' + escapeHtml(r.status) + ''; - var bmStatus = r.bm_status_text || (r.bm_status !== null ? "Code " + r.bm_status : ""); + var bmStatusVal = bm ? bm.status_text || (bm.status !== null ? "Code " + bm.status : "") : r.bm_status_text || (r.bm_status !== null ? "Code " + r.bm_status : ""); + var lastSeenVal = bm ? bm.last_seen : r.last_seen; + var hardwareVal = bm ? bm.hardware : r.hardware; tr.innerHTML = '' + @@ -875,9 +1070,9 @@ '' + '' + '' + - '' + - '' + - '' + + '' + + '' + + '' + ''; tr.addEventListener("click", function () {
' + r.id + '' + escapeHtml(r.country) + '' + statusTag + '' + escapeHtml(r.ts_linked) + '' + formatDate(r.last_seen) + '' + escapeHtml(bmStatus) + '' + escapeHtml(r.hardware) + '' + formatDate(lastSeenVal) + '' + escapeHtml(bmStatusVal) + '' + escapeHtml(hardwareVal || '') + '' + formatDate(r.last_polled) + '