Allow fetch bm for all repeaters, untag bm if not existing

This commit is contained in:
Marcus Kida 2026-02-11 10:18:08 +01:00
parent c1ce81456d
commit c088cf2cd8
3 changed files with 122 additions and 42 deletions

View file

@ -197,6 +197,7 @@ func handleAdminSaveBMData(db *sql.DB) http.HandlerFunc {
Agl int `json:"agl"` Agl int `json:"agl"`
Website string `json:"website"` Website string `json:"website"`
Description string `json:"description"` Description string `json:"description"`
LastKnownMaster int `json:"last_known_master"`
} }
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest) http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
@ -228,6 +229,42 @@ func handleAdminSaveBMData(db *sql.DB) http.HandlerFunc {
return 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.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`)) w.Write([]byte(`{"ok":true}`))
} }

View file

@ -46,6 +46,7 @@ func main() {
adminAPI.HandleFunc("/admin/api/repeaters/update", handleAdminUpdateRepeater(db)) adminAPI.HandleFunc("/admin/api/repeaters/update", handleAdminUpdateRepeater(db))
adminAPI.HandleFunc("/admin/api/repeaters/bm-device", handleBMDevice()) adminAPI.HandleFunc("/admin/api/repeaters/bm-device", handleBMDevice())
adminAPI.HandleFunc("/admin/api/repeaters/save-bm", handleAdminSaveBMData(db)) 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)) mux.Handle("/admin/api/", adminAuth(adminToken, adminAPI))
log.Println("Admin interface enabled at /admin/") log.Println("Admin interface enabled at /admin/")
} }

View file

@ -859,17 +859,15 @@
// --- Bulk BM fetch --- // --- Bulk BM fetch ---
function fetchAllBMData() { function fetchAllBMData() {
var bmRepeaters = currentRepeaters.filter(function (r) { if (!currentRepeaters.length) {
return r.networks && r.networks.indexOf("Brandmeister") !== -1; totalInfo.textContent = "No repeaters on this page";
});
if (!bmRepeaters.length) {
totalInfo.textContent = "No BrandMeister repeaters on this page";
return; return;
} }
var allRepeaters = currentRepeaters;
pendingBMData = {}; pendingBMData = {};
fetchAllBmBtn.disabled = true; fetchAllBmBtn.disabled = true;
fetchAllBmBtn.textContent = "Fetching 0/" + bmRepeaters.length; fetchAllBmBtn.textContent = "Fetching 0/" + allRepeaters.length;
var idx = 0; var idx = 0;
var changed = 0; var changed = 0;
@ -880,13 +878,13 @@
// 5s pause after every 10 fetches, otherwise 500ms (2/sec max) // 5s pause after every 10 fetches, otherwise 500ms (2/sec max)
var delay = (fetched > 0 && fetched % 10 === 0) ? 5000 : 500; var delay = (fetched > 0 && fetched % 10 === 0) ? 5000 : 500;
if (fetched > 0 && fetched % 10 === 0) { if (fetched > 0 && fetched % 10 === 0) {
fetchAllBmBtn.textContent = "Pausing... " + idx + "/" + bmRepeaters.length; fetchAllBmBtn.textContent = "Pausing... " + idx + "/" + allRepeaters.length;
} }
setTimeout(next, delay); setTimeout(next, delay);
} }
function next() { function next() {
if (idx >= bmRepeaters.length) { if (idx >= allRepeaters.length) {
fetchAllBmBtn.disabled = false; fetchAllBmBtn.disabled = false;
fetchAllBmBtn.textContent = "Fetch All BM"; fetchAllBmBtn.textContent = "Fetch All BM";
if (changed > 0) { if (changed > 0) {
@ -899,8 +897,8 @@
return; return;
} }
var r = bmRepeaters[idx]; var r = allRepeaters[idx];
fetchAllBmBtn.textContent = "Fetching " + (idx + 1) + "/" + bmRepeaters.length; fetchAllBmBtn.textContent = "Fetching " + (idx + 1) + "/" + allRepeaters.length;
apiFetch("/admin/api/repeaters/bm-device?id=" + r.id) apiFetch("/admin/api/repeaters/bm-device?id=" + r.id)
.then(function (bm) { .then(function (bm) {
@ -914,6 +912,13 @@
(bm.website || "") !== (r.website || "") || (bm.website || "") !== (r.website || "") ||
(bm.description || "") !== (r.description || ""); (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) { if (hasChange) {
pendingBMData[r.id] = bm; pendingBMData[r.id] = bm;
changed++; changed++;
@ -924,7 +929,15 @@
}) })
.catch(function (err) { .catch(function (err) {
if (err.message === "unauthorized") return; if (err.message === "unauthorized") return;
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++; errors++;
}
idx++; idx++;
fetched++; fetched++;
scheduleNext(); scheduleNext();
@ -963,7 +976,15 @@
var bm = pendingBMData[id]; var bm = pendingBMData[id];
bmReviewText.textContent = "Saving " + (idx + 1) + "/" + ids.length + "..."; bmReviewText.textContent = "Saving " + (idx + 1) + "/" + ids.length + "...";
apiFetch("/admin/api/repeaters/save-bm", { 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", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
@ -976,10 +997,12 @@
pep: bm.pep || 0, pep: bm.pep || 0,
agl: bm.agl || 0, agl: bm.agl || 0,
website: bm.website || "", website: bm.website || "",
description: bm.description || "" description: bm.description || "",
last_known_master: bm.last_known_master || 0
}) })
}) });
.then(function () { saved++; idx++; nextSave(); }) }
req.then(function () { saved++; idx++; nextSave(); })
.catch(function (err) { .catch(function (err) {
if (err.message === "unauthorized") return; if (err.message === "unauthorized") return;
errors++; errors++;
@ -1042,7 +1065,11 @@
repeaters.forEach(function (r, idx) { repeaters.forEach(function (r, idx) {
var tr = document.createElement("tr"); var tr = document.createElement("tr");
var bm = pendingBMData[r.id]; 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" var bandTag = r.band === "2m"
? '<span class="tag tag-2m">2m</span>' ? '<span class="tag tag-2m">2m</span>'
@ -1052,9 +1079,9 @@
? '<span class="tag tag-inactive">Inactive</span>' ? '<span class="tag tag-inactive">Inactive</span>'
: '<span class="tag tag-active">' + escapeHtml(r.status) + '</span>'; : '<span class="tag tag-active">' + escapeHtml(r.status) + '</span>';
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 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.last_seen : r.last_seen; var lastSeenVal = (bm && !bm._remove_bm_tag) ? bm.last_seen : r.last_seen;
var hardwareVal = bm ? bm.hardware : r.hardware; var hardwareVal = (bm && !bm._remove_bm_tag) ? bm.hardware : r.hardware;
tr.innerHTML = tr.innerHTML =
'<td>' + r.id + '</td>' + '<td>' + r.id + '</td>' +
@ -1359,6 +1386,21 @@
} }
editingRepeater.bm_status = bm.status; editingRepeater.bm_status = bm.status;
editingRepeater.bm_status_text = bm.status_text || ""; 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"; saveStatus.textContent = "BM data loaded — review and save";