diff --git a/admin.go b/admin.go index 279bf81..8a4ccc4 100644 --- a/admin.go +++ b/admin.go @@ -187,16 +187,17 @@ func handleAdminSaveBMData(db *sql.DB) http.HandlerFunc { } 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"` + 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"` + LastKnownMaster int `json:"last_known_master"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest) @@ -228,6 +229,42 @@ func handleAdminSaveBMData(db *sql.DB) http.HandlerFunc { return } + // Add Brandmeister tag if valid master server + if payload.LastKnownMaster != 0 && payload.LastKnownMaster != 9999 { + db.Exec(`UPDATE repeaters SET networks = array_append(networks, 'Brandmeister') + WHERE id = $1 AND NOT networks @> ARRAY['Brandmeister']`, payload.ID) + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) + } +} + +func handleAdminRemoveBMTag(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"` + } + 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 + } + + _, err := db.Exec(`UPDATE repeaters SET networks = array_remove(networks, 'Brandmeister'), last_polled=NOW() WHERE id = $1`, payload.ID) + if err != nil { + http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) } diff --git a/main.go b/main.go index eaf2b24..aef3faa 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ func main() { 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)) + adminAPI.HandleFunc("/admin/api/repeaters/remove-bm-tag", handleAdminRemoveBMTag(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 432ba30..b37e7c0 100644 --- a/static/admin.html +++ b/static/admin.html @@ -859,17 +859,15 @@ // --- 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"; + if (!currentRepeaters.length) { + totalInfo.textContent = "No repeaters on this page"; return; } + var allRepeaters = currentRepeaters; pendingBMData = {}; fetchAllBmBtn.disabled = true; - fetchAllBmBtn.textContent = "Fetching 0/" + bmRepeaters.length; + fetchAllBmBtn.textContent = "Fetching 0/" + allRepeaters.length; var idx = 0; var changed = 0; @@ -880,13 +878,13 @@ // 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; + fetchAllBmBtn.textContent = "Pausing... " + idx + "/" + allRepeaters.length; } setTimeout(next, delay); } function next() { - if (idx >= bmRepeaters.length) { + if (idx >= allRepeaters.length) { fetchAllBmBtn.disabled = false; fetchAllBmBtn.textContent = "Fetch All BM"; if (changed > 0) { @@ -899,8 +897,8 @@ return; } - var r = bmRepeaters[idx]; - fetchAllBmBtn.textContent = "Fetching " + (idx + 1) + "/" + bmRepeaters.length; + var r = allRepeaters[idx]; + fetchAllBmBtn.textContent = "Fetching " + (idx + 1) + "/" + allRepeaters.length; apiFetch("/admin/api/repeaters/bm-device?id=" + r.id) .then(function (bm) { @@ -914,6 +912,13 @@ (bm.website || "") !== (r.website || "") || (bm.description || "") !== (r.description || ""); + // If valid BM master and repeater lacks Brandmeister tag, mark as changed + var validMaster = bm.last_known_master && bm.last_known_master !== 0 && bm.last_known_master !== 9999; + if (validMaster && (!r.networks || r.networks.indexOf("Brandmeister") === -1)) { + hasChange = true; + bm._add_bm_tag = true; + } + if (hasChange) { pendingBMData[r.id] = bm; changed++; @@ -924,7 +929,15 @@ }) .catch(function (err) { if (err.message === "unauthorized") return; - errors++; + if (err.message === "HTTP 404") { + // Not on BrandMeister — if it has the tag, mark for removal + if (r.networks && r.networks.indexOf("Brandmeister") !== -1) { + pendingBMData[r.id] = { _remove_bm_tag: true }; + changed++; + } + } else { + errors++; + } idx++; fetched++; scheduleNext(); @@ -963,23 +976,33 @@ 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(); }) + var req; + if (bm._remove_bm_tag) { + req = apiFetch("/admin/api/repeaters/remove-bm-tag", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: id }) + }); + } else { + req = 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 || "", + last_known_master: bm.last_known_master || 0 + }) + }); + } + req.then(function () { saved++; idx++; nextSave(); }) .catch(function (err) { if (err.message === "unauthorized") return; errors++; @@ -1042,7 +1065,11 @@ repeaters.forEach(function (r, idx) { var tr = document.createElement("tr"); var bm = pendingBMData[r.id]; - if (bm) tr.className = "bm-changed"; + if (bm && bm._remove_bm_tag) { + tr.className = "bm-error"; + } else if (bm) { + tr.className = "bm-changed"; + } var bandTag = r.band === "2m" ? '2m' @@ -1052,9 +1079,9 @@ ? 'Inactive' : '' + escapeHtml(r.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; + var bmStatusVal = (bm && !bm._remove_bm_tag) ? 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._remove_bm_tag) ? bm.last_seen : r.last_seen; + var hardwareVal = (bm && !bm._remove_bm_tag) ? bm.hardware : r.hardware; tr.innerHTML = '' + r.id + '' + @@ -1359,6 +1386,21 @@ } editingRepeater.bm_status = bm.status; editingRepeater.bm_status_text = bm.status_text || ""; + + // Auto-add Brandmeister tag if valid master + var validMaster = bm.last_known_master && bm.last_known_master !== 0 && bm.last_known_master !== 9999; + if (validMaster && (!editingRepeater.networks || editingRepeater.networks.indexOf("Brandmeister") === -1)) { + if (!editingRepeater.networks) editingRepeater.networks = []; + editingRepeater.networks.push("Brandmeister"); + // Check the Brandmeister checkbox in the form + var netGroup = detailBody.querySelector('[data-key="networks"]'); + if (netGroup) { + var cbs = netGroup.querySelectorAll('input[type="checkbox"]'); + for (var ci = 0; ci < cbs.length; ci++) { + if (cbs[ci].value === "Brandmeister") cbs[ci].checked = true; + } + } + } } saveStatus.textContent = "BM data loaded — review and save";