mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
Surface the event-bus WebSocket state in the UI
SDK (sdk-event-bus amended on the fork): a new `WsState` enum (`Stopped`/`Connecting`/`Connected`/`Reconnecting`) is broadcast through a `watch::Sender` inside `EventBusClient`, exposed via `state()` for observers. Transitions are emitted at every step of the reconnect loop, plus a `Drop` guard guarantees a final `Stopped` even on a panic in the caller's task tree. Two new SDK tests cover the initial value and multi-subscriber broadcast. Bridge: capture `bus_client.state()` at start, mirror it into a serde `WsStatus` field on `BridgeStats`, and clear on stop. UI: add a third stat card "Realtime" with a colored dot (green Connected, orange pulsing Connecting/Reconnecting, gray Off) reading `stats.ws_status`. Stats grid switches from 2 to 3 columns. Submodule pointer bumped to the rebuilt `tutabridge-integration` which cherry-picks all six SDK branches on `upstream/master`; the move-mails fixup (use `make_test_facade` so its test compiles alongside the blob branch) is re-applied. 341/341 SDK lib tests pass, 154/154 bridge tests pass.
This commit is contained in:
Generated
+3
-3
@@ -781,7 +781,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-primitives"
|
name = "crypto-primitives"
|
||||||
version = "348.260526.0"
|
version = "348.260528.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -5304,7 +5304,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tuta-sdk"
|
name = "tuta-sdk"
|
||||||
version = "348.260526.0"
|
version = "348.260528.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_log",
|
"android_log",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -5657,7 +5657,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "348.260526.0"
|
version = "348.260528.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -52,10 +52,32 @@ pub enum BridgeStatus {
|
|||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Live state of the event-bus WebSocket. Surfaces directly in the UI so the
|
||||||
|
/// user can see at a glance whether realtime push is healthy.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||||
|
pub enum WsStatus {
|
||||||
|
Stopped,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Reconnecting,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tutasdk::event_bus::WsState> for WsStatus {
|
||||||
|
fn from(s: tutasdk::event_bus::WsState) -> Self {
|
||||||
|
match s {
|
||||||
|
tutasdk::event_bus::WsState::Stopped => Self::Stopped,
|
||||||
|
tutasdk::event_bus::WsState::Connecting => Self::Connecting,
|
||||||
|
tutasdk::event_bus::WsState::Connected => Self::Connected,
|
||||||
|
tutasdk::event_bus::WsState::Reconnecting => Self::Reconnecting,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
pub struct BridgeStats {
|
pub struct BridgeStats {
|
||||||
pub uptime_secs: Option<u64>,
|
pub uptime_secs: Option<u64>,
|
||||||
pub mails_synced: usize,
|
pub mails_synced: usize,
|
||||||
|
pub ws_status: WsStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BridgeHandle {
|
pub struct BridgeHandle {
|
||||||
@@ -65,6 +87,8 @@ pub struct BridgeHandle {
|
|||||||
started_at: Option<std::time::Instant>,
|
started_at: Option<std::time::Instant>,
|
||||||
store: Option<Arc<MailStore>>,
|
store: Option<Arc<MailStore>>,
|
||||||
task: Option<tokio::task::JoinHandle<()>>,
|
task: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
/// Latest event-bus state, populated at `start` and observed by `stats`.
|
||||||
|
ws_state_rx: Option<watch::Receiver<tutasdk::event_bus::WsState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BridgeHandle {
|
impl BridgeHandle {
|
||||||
@@ -77,6 +101,7 @@ impl BridgeHandle {
|
|||||||
started_at: None,
|
started_at: None,
|
||||||
store: None,
|
store: None,
|
||||||
task: None,
|
task: None,
|
||||||
|
ws_state_rx: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +122,15 @@ impl BridgeHandle {
|
|||||||
Some(store) => store.total_mail_count().await,
|
Some(store) => store.total_mail_count().await,
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
let ws_status = self
|
||||||
|
.ws_state_rx
|
||||||
|
.as_ref()
|
||||||
|
.map(|rx| WsStatus::from(*rx.borrow()))
|
||||||
|
.unwrap_or(WsStatus::Stopped);
|
||||||
BridgeStats {
|
BridgeStats {
|
||||||
uptime_secs: self.started_at.map(|t| t.elapsed().as_secs()),
|
uptime_secs: self.started_at.map(|t| t.elapsed().as_secs()),
|
||||||
mails_synced: count,
|
mails_synced: count,
|
||||||
|
ws_status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +266,7 @@ impl BridgeHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let bus_ids_for_handler = bus_client.last_batch_ids();
|
let bus_ids_for_handler = bus_client.last_batch_ids();
|
||||||
|
self.ws_state_rx = Some(bus_client.state());
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
let imap_tls = tls_acceptor.clone();
|
let imap_tls = tls_acceptor.clone();
|
||||||
@@ -343,6 +375,7 @@ impl BridgeHandle {
|
|||||||
let _ = task.await;
|
let _ = task.await;
|
||||||
}
|
}
|
||||||
self.started_at = None;
|
self.started_at = None;
|
||||||
|
self.ws_state_rx = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_log(&self, msg: &str) {
|
fn emit_log(&self, msg: &str) {
|
||||||
|
|||||||
+1
-1
Submodule tuta-repo updated: e65ec9fa5a...335b75d781
+24
-1
@@ -230,10 +230,33 @@
|
|||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ws-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: var(--brand);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--on-surface);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot.pulsing {
|
||||||
|
animation: breathe 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { BridgeStatus, BridgeStats } from "../types";
|
import type { BridgeStatus, BridgeStats, WsStatus } from "../types";
|
||||||
import { isError } from "../types";
|
import { isError } from "../types";
|
||||||
|
|
||||||
|
function wsBadge(status: WsStatus): { color: string; label: string; pulsing: boolean } {
|
||||||
|
switch (status) {
|
||||||
|
case "Connected":
|
||||||
|
return { color: "var(--green)", label: "Connected", pulsing: false };
|
||||||
|
case "Connecting":
|
||||||
|
return { color: "var(--orange)", label: "Connecting…", pulsing: true };
|
||||||
|
case "Reconnecting":
|
||||||
|
return { color: "var(--orange)", label: "Reconnecting…", pulsing: true };
|
||||||
|
case "Stopped":
|
||||||
|
default:
|
||||||
|
return { color: "var(--gray)", label: "Off", pulsing: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: BridgeStatus | null;
|
status: BridgeStatus | null;
|
||||||
stats: BridgeStats;
|
stats: BridgeStats;
|
||||||
@@ -122,6 +136,23 @@ export function Dashboard({ status, stats, hasSavedSession, loading, onStart, on
|
|||||||
</span>
|
</span>
|
||||||
<span className="stat-label">Uptime</span>
|
<span className="stat-label">Uptime</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<span className="ws-badge">
|
||||||
|
{(() => {
|
||||||
|
const b = wsBadge(stats.ws_status);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={"ws-dot" + (b.pulsing ? " pulsing" : "")}
|
||||||
|
style={{ background: b.color }}
|
||||||
|
/>
|
||||||
|
<span className="ws-text">{b.label}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<span className="stat-label">Realtime</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ const POLL_INTERVAL = 1000;
|
|||||||
export function useBridge() {
|
export function useBridge() {
|
||||||
const [config, setConfig] = useState<Config | null>(null);
|
const [config, setConfig] = useState<Config | null>(null);
|
||||||
const [status, setStatus] = useState<BridgeStatus | null>(null);
|
const [status, setStatus] = useState<BridgeStatus | null>(null);
|
||||||
const [stats, setStats] = useState<BridgeStats>({ uptime_secs: null, mails_synced: 0 });
|
const [stats, setStats] = useState<BridgeStats>({
|
||||||
|
uptime_secs: null,
|
||||||
|
mails_synced: 0,
|
||||||
|
ws_status: "Stopped",
|
||||||
|
});
|
||||||
const [hasSavedSession, setHasSavedSession] = useState(false);
|
const [hasSavedSession, setHasSavedSession] = useState(false);
|
||||||
const [bridgePassword, setBridgePassword] = useState<string | null>(null);
|
const [bridgePassword, setBridgePassword] = useState<string | null>(null);
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ export interface Config {
|
|||||||
|
|
||||||
export type BridgeStatus = "Stopped" | "Starting" | "Running" | { Error: string };
|
export type BridgeStatus = "Stopped" | "Starting" | "Running" | { Error: string };
|
||||||
|
|
||||||
|
export type WsStatus = "Stopped" | "Connecting" | "Connected" | "Reconnecting";
|
||||||
|
|
||||||
export interface BridgeStats {
|
export interface BridgeStats {
|
||||||
uptime_secs: number | null;
|
uptime_secs: number | null;
|
||||||
mails_synced: number;
|
mails_synced: number;
|
||||||
|
ws_status: WsStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusLabel(status: BridgeStatus): string {
|
export function statusLabel(status: BridgeStatus): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user