diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
index 9bfc122..a831b90 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
@@ -1011,7 +1011,10 @@ async function refreshRigList() {
if (typeof r.display_name === "string" && r.display_name.length > 0) {
displayNames[r.remote] = r.display_name;
} else {
- displayNames[r.remote] = r.remote;
+ const mfg = (r.manufacturer || "").trim();
+ const mdl = (r.model || "").trim();
+ const hw = [mfg, mdl].filter(Boolean).join(" ");
+ displayNames[r.remote] = hw || r.remote;
}
});
serverRigs = rigs;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index a10b923..1cc0bdb 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -379,11 +379,11 @@
-
+
-
+
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js
index a5c950a..8ddaa80 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js
@@ -3,16 +3,17 @@
/** Current bookmark scope: "general" or a rig remote name. */
let bmScope = "general";
-/** Build the ?scope= query string for the current bookmark scope. */
-function bmScopeParam(prefix) {
+/** Build the ?scope= query string for a given or current bookmark scope. */
+function bmScopeParam(prefix, scope) {
const sep = prefix ? "&" : "?";
- return sep + "scope=" + encodeURIComponent(bmScope);
+ return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
}
var bmList = [];
var bmRevision = 0;
let bmFilteredList = [];
let bmEditId = null;
+let bmEditScope = null;
let bmCurrentPage = 1;
const BM_PAGE_SIZE = 25;
const bmSelected = new Set();
@@ -154,6 +155,7 @@ function bmRender(list) {
const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
const pageItems = list.slice(startIndex, endIndex);
+ const showScope = bmScope !== "general";
pageItems.forEach((bm) => {
const tr = document.createElement("tr");
tr.dataset.bmId = bm.id;
@@ -163,9 +165,10 @@ function bmRender(list) {
const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
const commentCell = bm.comment || "";
const checked = bmSelected.has(bm.id) ? " checked" : "";
+ const scopeBadge = showScope && bm.scope === "general" ? ' G' : "";
tr.innerHTML =
`
| ` +
- `${bmEsc(bm.name)} | ` +
+ `${bmEsc(bm.name)}${scopeBadge} | ` +
`${bmFmtFreq(bm.freq_hz)} | ` +
`${bmEsc(bm.mode)} | ` +
`${bwCell} | ` +
@@ -228,6 +231,7 @@ function bmOpenForm(bm) {
const wrap = document.getElementById("bm-form-wrap");
if (!wrap) return;
bmEditId = bm ? bm.id : null;
+ bmEditScope = bm ? (bm.scope || bmScope) : null;
document.getElementById("bm-id").value = bm ? bm.id : "";
document.getElementById("bm-name").value = bm ? bm.name : "";
@@ -296,7 +300,7 @@ async function bmSave(e) {
try {
let resp;
if (id) {
- resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
+ resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, bmEditScope), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
@@ -325,8 +329,10 @@ async function bmSave(e) {
async function bmDelete(id) {
if (!confirm("Delete this bookmark?")) return;
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm ? bm.scope : undefined;
try {
- const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
+ const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
method: "DELETE",
});
if (!resp.ok) throw new Error("HTTP " + resp.status);
@@ -471,12 +477,21 @@ async function bmMoveSelected() {
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
if (!confirm(`Move ${ids.length} bookmark${ids.length > 1 ? "s" : ""} to "${targetLabel}"?`)) return;
try {
- const resp = await fetch("/bookmarks/batch_move" + bmScopeParam(false), {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ ids, to: target }),
- });
- if (!resp.ok) throw new Error("HTTP " + resp.status);
+ // Group selected IDs by their owning scope (skip if already in target).
+ const byScope = {};
+ for (const id of ids) {
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm?.scope || bmScope;
+ if (scope === target) continue;
+ (byScope[scope] ||= []).push(id);
+ }
+ await Promise.all(Object.entries(byScope).map(([scope, scopeIds]) =>
+ fetch("/bookmarks/batch_move" + bmScopeParam(false, scope), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: scopeIds, to: target }),
+ }).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
+ ));
bmSelected.clear();
bmUpdateSelectionUi();
await bmFetch(document.getElementById("bm-category-filter").value);
@@ -505,12 +520,20 @@ async function bmDeleteSelected() {
if (ids.length === 0) return;
if (!confirm(`Delete ${ids.length} selected bookmark${ids.length > 1 ? "s" : ""}?`)) return;
try {
- const resp = await fetch("/bookmarks/batch_delete" + bmScopeParam(false), {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ ids }),
- });
- if (!resp.ok) throw new Error("HTTP " + resp.status);
+ // Group selected IDs by their owning scope.
+ const byScope = {};
+ for (const id of ids) {
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm?.scope || bmScope;
+ (byScope[scope] ||= []).push(id);
+ }
+ await Promise.all(Object.entries(byScope).map(([scope, scopeIds]) =>
+ fetch("/bookmarks/batch_delete" + bmScopeParam(false, scope), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: scopeIds }),
+ }).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
+ ));
bmSelected.clear();
bmUpdateSelectionUi();
await bmFetch(document.getElementById("bm-category-filter").value);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index 11eea84..e502dc7 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -3725,17 +3725,19 @@ button:focus-visible, input:focus-visible, select:focus-visible {
align-items: center;
gap: 0.35rem;
}
-.bm-toolbar-btn {
- white-space: nowrap;
- font-size: 0.78rem;
- padding: 0.25rem 0.6rem;
- border-radius: 0.25rem;
- border: 1px solid var(--border-light);
- background: var(--btn-bg);
- color: var(--text-heading);
- cursor: pointer;
-}
+.bm-scope-badge {
+ display: inline-block;
+ font-size: 0.65rem;
+ font-weight: 700;
+ padding: 0.05rem 0.3rem;
+ border-radius: 0.2rem;
+ background: var(--btn-bg);
+ border: 1px solid var(--border-light);
+ color: var(--text-muted);
+ vertical-align: middle;
+ margin-left: 0.3rem;
+}
.bm-empty {
padding: 2rem 1rem;
text-align: center;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
index 871f677..6eda16c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
@@ -1505,6 +1505,14 @@ where
.body(body)
}
+/// A bookmark with its owning scope tag for the list response.
+#[derive(serde::Serialize)]
+struct BookmarkWithScope {
+ #[serde(flatten)]
+ bm: crate::server::bookmarks::Bookmark,
+ scope: String,
+}
+
#[get("/bookmarks")]
pub async fn list_bookmarks(
req: HttpRequest,
@@ -1517,15 +1525,38 @@ pub async fn list_bookmarks(
status::index_html(),
));
}
- let store = resolve_bookmark_store(query.scope.as_deref(), store_map.get_ref());
- let mut list = store.list();
+ let scope = query.scope.as_deref().filter(|s| !s.is_empty() && *s != "general");
+ let mut list: Vec = match scope {
+ Some(remote) => {
+ // Rig selected: merge general + rig-specific (rig wins on duplicate IDs).
+ let mut map: std::collections::HashMap = store_map
+ .general()
+ .list()
+ .into_iter()
+ .map(|bm| {
+ let id = bm.id.clone();
+ (id, BookmarkWithScope { bm, scope: "general".into() })
+ })
+ .collect();
+ for bm in store_map.store_for(remote).list() {
+ let id = bm.id.clone();
+ map.insert(id, BookmarkWithScope { bm, scope: remote.to_owned() });
+ }
+ map.into_values().collect()
+ }
+ None => {
+ store_map.general().list().into_iter()
+ .map(|bm| BookmarkWithScope { bm, scope: "general".into() })
+ .collect()
+ }
+ };
if let Some(ref cat) = query.category {
if !cat.is_empty() {
let cat_lower = cat.to_lowercase();
- list.retain(|bm| bm.category.to_lowercase() == cat_lower);
+ list.retain(|item| item.bm.category.to_lowercase() == cat_lower);
}
}
- list.sort_by_key(|bm| bm.freq_hz);
+ list.sort_by_key(|item| item.bm.freq_hz);
Ok(HttpResponse::Ok().json(list))
}