Updated the dashboard page

This commit is contained in:
Connor Johnstone
2026-03-18 11:42:04 -04:00
parent 55607df07b
commit 2314346925
6 changed files with 317 additions and 60 deletions

View File

@@ -1,3 +1,4 @@
use gloo_timers::callback::Interval;
use yew::prelude::*;
use crate::api;
@@ -8,20 +9,139 @@ use crate::types::Status;
pub fn dashboard() -> Html {
let status = use_state(|| None::<Status>);
let error = use_state(|| None::<String>);
let message = use_state(|| None::<String>);
{
// Fetch status function
let fetch_status = {
let status = status.clone();
let error = error.clone();
use_effect_with((), move |_| {
Callback::from(move |_: ()| {
let status = status.clone();
let error = error.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::get_status().await {
Ok(s) => status.set(Some(s)),
Ok(s) => {
error.set(None);
status.set(Some(s));
}
Err(e) => error.set(Some(e.0)),
}
});
})
};
// Initial fetch
{
let fetch = fetch_status.clone();
use_effect_with((), move |_| {
fetch.emit(());
});
}
// Auto-refresh every 5 seconds
{
let fetch = fetch_status.clone();
use_effect_with((), move |_| {
let interval = Interval::new(1_000, move || {
fetch.emit(());
});
// Keep the interval alive for the component's lifetime
move || drop(interval)
});
}
// Action handlers
let on_sync = {
let message = message.clone();
let error = error.clone();
let fetch = fetch_status.clone();
Callback::from(move |_: MouseEvent| {
let message = message.clone();
let error = error.clone();
let fetch = fetch.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::sync_downloads().await {
Ok(s) => {
message.set(Some(format!("Synced: {} enqueued, {} skipped", s.enqueued, s.skipped)));
fetch.emit(());
}
Err(e) => error.set(Some(e.0)),
}
});
})
};
let on_process = {
let message = message.clone();
let error = error.clone();
let fetch = fetch_status.clone();
Callback::from(move |_: MouseEvent| {
let message = message.clone();
let error = error.clone();
let fetch = fetch.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::process_downloads().await {
Ok(t) => {
message.set(Some(format!("Downloads started (task: {})", &t.task_id[..8])));
fetch.emit(());
}
Err(e) => error.set(Some(e.0)),
}
});
})
};
let on_index = {
let message = message.clone();
let error = error.clone();
let fetch = fetch_status.clone();
Callback::from(move |_: MouseEvent| {
let message = message.clone();
let error = error.clone();
let fetch = fetch.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::trigger_index().await {
Ok(t) => { message.set(Some(format!("Indexing started ({})", &t.task_id[..8]))); fetch.emit(()); }
Err(e) => error.set(Some(e.0)),
}
});
})
};
let on_tag = {
let message = message.clone();
let error = error.clone();
let fetch = fetch_status.clone();
Callback::from(move |_: MouseEvent| {
let message = message.clone();
let error = error.clone();
let fetch = fetch.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::trigger_tag().await {
Ok(t) => { message.set(Some(format!("Tagging started ({})", &t.task_id[..8]))); fetch.emit(()); }
Err(e) => error.set(Some(e.0)),
}
});
})
};
let on_organize = {
let message = message.clone();
let error = error.clone();
let fetch = fetch_status.clone();
Callback::from(move |_: MouseEvent| {
let message = message.clone();
let error = error.clone();
let fetch = fetch.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::trigger_organize().await {
Ok(t) => { message.set(Some(format!("Organizing started ({})", &t.task_id[..8]))); fetch.emit(()); }
Err(e) => error.set(Some(e.0)),
}
});
})
};
if let Some(ref err) = *error {
return html! { <div class="error">{ format!("Error: {err}") }</div> };
}
@@ -36,6 +156,13 @@ pub fn dashboard() -> Html {
<h2>{ "Dashboard" }</h2>
</div>
if let Some(ref msg) = *message {
<div class="card" style="border-color: var(--success);">
<p>{ msg }</p>
</div>
}
// Stats grid
<div class="stats-grid">
<div class="stat-card">
<div class="value">{ s.library.total_items }</div>
@@ -55,34 +182,117 @@ pub fn dashboard() -> Html {
</div>
</div>
// Actions
<div class="card">
<h3>{ "Download Queue" }</h3>
<p>{ format!("{} pending, {} downloading", s.queue.pending, s.queue.downloading) }</p>
<h3>{ "Actions" }</h3>
<div class="actions mt-1">
<button class="btn btn-primary" onclick={on_sync}>{ "Sync Watchlist" }</button>
<button class="btn btn-success" onclick={on_process}>{ "Process Downloads" }</button>
<button class="btn btn-secondary" onclick={on_index}>{ "Re-index" }</button>
<button class="btn btn-secondary" onclick={on_tag}>{ "Auto-tag" }</button>
<button class="btn btn-secondary" onclick={on_organize}>{ "Organize" }</button>
</div>
</div>
// Background Tasks
if !s.tasks.is_empty() {
<div class="card">
<h3>{ "Background Tasks" }</h3>
<table>
<thead>
<tr>
<th>{ "Type" }</th>
<th>{ "Status" }</th>
<th>{ "Result" }</th>
</tr>
<tr><th>{ "Type" }</th><th>{ "Status" }</th><th>{ "Progress" }</th><th>{ "Result" }</th></tr>
</thead>
<tbody>
{ for s.tasks.iter().map(|t| html! {
<tr>
<td>{ &t.task_type }</td>
<td><StatusBadge status={t.status.clone()} /></td>
<td class="text-sm text-muted">{ t.result.as_deref().unwrap_or("") }</td>
</tr>
{ for s.tasks.iter().map(|t| {
let progress_html = if let Some(ref p) = t.progress {
if p.total > 0 {
let pct = (p.current as f64 / p.total as f64 * 100.0) as u32;
html! {
<div>
<div class="text-sm">{ &p.message }{ format!(" ({}/{})", p.current, p.total) }</div>
<div style="background: var(--bg-card); border-radius: 4px; height: 8px; margin-top: 4px;">
<div style={format!("background: var(--accent); border-radius: 4px; height: 100%; width: {pct}%;")}></div>
</div>
</div>
}
} else {
html! { <span class="text-sm text-muted">{ &p.message }</span> }
}
} else {
html! {}
};
html! {
<tr>
<td>{ &t.task_type }</td>
<td><StatusBadge status={t.status.clone()} /></td>
<td>{ progress_html }</td>
<td class="text-sm text-muted">{ t.result.as_deref().unwrap_or("") }</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
// Download Queue Items
if !s.queue.items.is_empty() {
<div class="card">
<h3>{ format!("Download Queue ({} pending, {} downloading, {} failed)",
s.queue.pending, s.queue.downloading, s.queue.failed) }</h3>
<table>
<thead>
<tr><th>{ "Query" }</th><th>{ "Status" }</th><th>{ "Error" }</th></tr>
</thead>
<tbody>
{ for s.queue.items.iter().map(|item| html! {
<tr>
<td>{ &item.query }</td>
<td><StatusBadge status={item.status.clone()} /></td>
<td class="text-sm text-muted">{ item.error_message.as_deref().unwrap_or("") }</td>
</tr>
})}
</tbody>
</table>
</div>
} else if s.queue.pending > 0 || s.queue.downloading > 0 {
<div class="card">
<h3>{ "Download Queue" }</h3>
<p>{ format!("{} pending, {} downloading", s.queue.pending, s.queue.downloading) }</p>
</div>
}
// Tagging Queue
if let Some(ref tagging) = s.tagging {
if tagging.needs_tagging > 0 {
<div class="card">
<h3>{ format!("Tagging Queue ({} tracks need metadata)", tagging.needs_tagging) }</h3>
if !tagging.items.is_empty() {
<table>
<thead>
<tr><th>{ "Title" }</th><th>{ "Artist" }</th><th>{ "Album" }</th><th>{ "MBID" }</th></tr>
</thead>
<tbody>
{ for tagging.items.iter().map(|t| html! {
<tr>
<td>{ t.title.as_deref().unwrap_or("Unknown") }</td>
<td>{ t.artist.as_deref().unwrap_or("") }</td>
<td class="text-muted">{ t.album.as_deref().unwrap_or("") }</td>
<td class="text-sm text-muted">
if t.album.is_some() {
{ "needs album link" }
} else {
{ "needs metadata" }
}
</td>
</tr>
})}
</tbody>
</table>
}
</div>
}
}
</div>
}
}