[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:
2026-02-08 13:59:51 +01:00
parent d7d5674683
commit 7eaa39ea4a
4 changed files with 36 additions and 3 deletions
@@ -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,
} }
} }