[feat](trx-frontend-http): add Plugins tab showing registered frontends
Add GET /frontends API endpoint returning registered frontend names as JSON. Add Plugins tab to the web UI that fetches and displays the list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -615,6 +615,18 @@ document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
|||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
|
// --- Plugins tab ---
|
||||||
|
fetch("/frontends").then(r => r.json()).then(names => {
|
||||||
|
const list = document.getElementById("plugins-list");
|
||||||
|
if (!Array.isArray(names) || names.length === 0) {
|
||||||
|
list.innerHTML = '<div class="plugin-item" style="color:var(--text-muted);">No frontends registered</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = names.map(n => `<div class="plugin-item">${n}</div>`).join("");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to fetch frontends", err);
|
||||||
|
});
|
||||||
|
|
||||||
// --- Signal measurement ---
|
// --- Signal measurement ---
|
||||||
const sigMeasureBtn = document.getElementById("sig-measure-btn");
|
const sigMeasureBtn = document.getElementById("sig-measure-btn");
|
||||||
const sigClearBtn = document.getElementById("sig-clear-btn");
|
const sigClearBtn = document.getElementById("sig-clear-btn");
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<button class="tab active" data-tab="main">Main</button>
|
<button class="tab active" data-tab="main">Main</button>
|
||||||
|
<button class="tab" data-tab="plugins">Plugins</button>
|
||||||
<button class="tab" data-tab="about">About</button>
|
<button class="tab" data-tab="about">About</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-main" class="tab-panel">
|
<div id="tab-main" class="tab-panel">
|
||||||
@@ -116,6 +117,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tab-plugins" class="tab-panel" style="display:none;">
|
||||||
|
<div id="plugins-list"></div>
|
||||||
|
</div>
|
||||||
<div id="tab-about" class="tab-panel" style="display:none;">
|
<div id="tab-about" class="tab-panel" style="display:none;">
|
||||||
<table class="about-table">
|
<table class="about-table">
|
||||||
<tr><td>Server</td><td id="about-server-ver">--</td></tr>
|
<tr><td>Server</td><td id="about-server-ver">--</td></tr>
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ small { color: var(--text-muted); }
|
|||||||
.about-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
|
.about-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
|
||||||
.about-table tr:last-child td { border-bottom: none; }
|
.about-table tr:last-child td { border-bottom: none; }
|
||||||
.about-table td:first-child { color: var(--text-muted); width: 40%; }
|
.about-table td:first-child { color: var(--text-muted); width: 40%; }
|
||||||
|
.plugin-item { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); }
|
||||||
|
.plugin-item:last-child { border-bottom: none; }
|
||||||
.footer { display: flex; justify-content: space-between; align-items: baseline; margin-top: 1.1rem; }
|
.footer { display: flex; justify-content: space-between; align-items: baseline; margin-top: 1.1rem; }
|
||||||
.full-row { grid-column: 1 / -1; }
|
.full-row { grid-column: 1 / -1; }
|
||||||
.copyright { color: var(--text-muted); font-size: 0.75rem; opacity: 0.7; }
|
.copyright { color: var(--text-muted); font-size: 0.75rem; opacity: 0.7; }
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ const FAVICON_BYTES: &[u8] = include_bytes!(concat!(
|
|||||||
const LOGO_BYTES: &[u8] =
|
const LOGO_BYTES: &[u8] =
|
||||||
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-logo.png"));
|
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-logo.png"));
|
||||||
|
|
||||||
|
#[get("/frontends")]
|
||||||
|
pub async fn frontends_api() -> Result<impl Responder, Error> {
|
||||||
|
let names = trx_frontend::registered_frontends();
|
||||||
|
Ok(HttpResponse::Ok().json(names))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/status")]
|
#[get("/status")]
|
||||||
pub async fn status_api(
|
pub async fn status_api(
|
||||||
state: web::Data<watch::Receiver<RigState>>,
|
state: web::Data<watch::Receiver<RigState>>,
|
||||||
@@ -229,6 +235,7 @@ pub async fn set_tx_limit(
|
|||||||
|
|
||||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index)
|
cfg.service(index)
|
||||||
|
.service(frontends_api)
|
||||||
.service(status_api)
|
.service(status_api)
|
||||||
.service(events)
|
.service(events)
|
||||||
.service(toggle_power)
|
.service(toggle_power)
|
||||||
@@ -322,9 +329,17 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
|||||||
return Ok(view);
|
return Ok(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
while rx.changed().await.is_ok() {
|
// Wait up to 5 seconds for a valid snapshot; fall back to a placeholder
|
||||||
if let Some(view) = rx.borrow().snapshot() {
|
// so the SSE stream starts immediately and the browser isn't left hanging.
|
||||||
return Ok(view);
|
let deadline = time::Instant::now() + Duration::from_secs(5);
|
||||||
|
loop {
|
||||||
|
match time::timeout_at(deadline, rx.changed()).await {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
if let Some(view) = rx.borrow().snapshot() {
|
||||||
|
return Ok(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user