Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/locales/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Няма записи OpenBridge от сървъра.",
"opb_activity": "Активни потоци",
"opb_status": "Статус",
"opb_status_online": "Онлайн",
"opb_status_offline": "Офлайн",
"toptg_no_data": "Все още няма данни за брой TG. Данните се записват при приключване на гласови QSO (продължителност > 5с).",
"mon_wait": "Изчакване на регистрация...",
"sslog_login_ip": "Вход по IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Sense entrades OpenBridge del servidor.",
"opb_activity": "Fluxos actius",
"opb_status": "Estat",
"opb_status_online": "En línia",
"opb_status_offline": "Fora de línia",
"toptg_no_data": "Encara no hi ha dades de recompte TG. Les dades es desen quan acaben els QSO de veu (durada > 5s).",
"mon_wait": "Esperant registre...",
"sslog_login_ip": "Entrar per IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Keine OpenBridge-Einträge vom Server.",
"opb_activity": "Aktive Streams",
"opb_status": "Status",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "Noch keine TG-Zähldaten. Daten werden gespeichert, wenn Sprach-QSOs enden (Dauer > 5s).",
"mon_wait": "Waiting for log...",
"tbl_count_bridges": "Number of Bridges",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Δεν υπάρχουν εγγραφές OpenBridge από τον διακομιστή.",
"opb_activity": "Ενεργές ροές",
"opb_status": "Κατάσταση",
"opb_status_online": "Σε σύνδεση",
"opb_status_offline": "Εκτός σύνδεσης",
"toptg_no_data": "Δεν υπάρχουν ακόμη δεδομένα αριθμού TG. Οι δεδομένα αποθηκεύονται όταν τερματίζουν οι φωνητικές QSO (διάρκεια > 5δ).",
"mon_wait": "Αναμονή καταγραφής...",
"sslog_login_ip": "Σύνδεση με IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "No OpenBridge entries from server.",
"opb_activity": "Active streams",
"opb_status": "Status",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "No TG count data yet. Data is saved when voice QSOs end (duration > 5s).",
"mon_wait": "Waiting for log...",
"sslog_login_ip": "Login by IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "No hay entradas OpenBridge del servidor.",
"opb_activity": "Flujos activos",
"opb_status": "Estado",
"opb_status_online": "En línea",
"opb_status_offline": "Fuera de línea",
"toptg_no_data": "Aún no hay datos de conteo TG. Se guardan cuando terminan QSO de voz (duración > 5s).",
"mon_wait": "Esperando registro...",
"sslog_login_ip": "Entrar por IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/et.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Serverist OpenBridge kirjeid pole.",
"opb_activity": "Aktiivsed voogud",
"opb_status": "Olek",
"opb_status_online": "Võrgus",
"opb_status_offline": "Võrgust väljas",
"toptg_no_data": "TG loenduse andmeid pole. Andmed salvestatakse, kui hääle QSO-d lõpevad (kestus > 5s).",
"mon_wait": "Ootan logi...",
"sslog_login_ip": "Logi sisse IP kaudu",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Aucune entrée OpenBridge du serveur.",
"opb_activity": "Flux actifs",
"opb_status": "Statut",
"opb_status_online": "En ligne",
"opb_status_offline": "Hors ligne",
"toptg_no_data": "Pas encore de données de décompte TG. Les données sont enregistrées à la fin des QSO voix (durée > 5 s).",
"mon_wait": "Waiting for log...",
"tbl_count_bridges": "Number of Bridges",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Nessuna voce OpenBridge dal server.",
"opb_activity": "Stream attivi",
"opb_status": "Stato",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "Ancora nessun dato di conteggio TG. I dati vengono salvati quando le QSO vocali terminano (durata > 5s).",
"mon_wait": "Waiting for log...",
"tbl_count_bridges": "Number of Bridges",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Geen OpenBridge-vermeldingen van de server.",
"opb_activity": "Actieve streams",
"opb_status": "Status",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "Nog geen TG-telgegevens. Gegevens worden opgeslagen wanneer spraak-QSO's eindigen (duur > 5s).",
"mon_wait": "Waiting for log...",
"tbl_count_bridges": "Number of Bridges",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Brak wpisów OpenBridge z serwera.",
"opb_activity": "Aktywne strumienie",
"opb_status": "Status",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "Brak danych liczby TG. Dane są zapisywane po zakończeniu QSO głosowych (czas > 5s).",
"mon_wait": "Oczekiwanie na dziennik...",
"sslog_login_ip": "Zaloguj po IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Sem entradas OpenBridge do servidor.",
"opb_activity": "Fluxos ativos",
"opb_status": "Estado",
"opb_status_online": "Online",
"opb_status_offline": "Offline",
"toptg_no_data": "Ainda sem dados de contagem TG. Os dados são guardados quando terminam QSO de voz (duração > 5s).",
"mon_wait": "Waiting for log...",
"tbl_count_bridges": "Number of Bridges",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/sr.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Нема уноса OpenBridge са сервера.",
"opb_activity": "Активни токови",
"opb_status": "Статус",
"opb_status_online": "На мрежи",
"opb_status_offline": "Ван мреже",
"toptg_no_data": "Још нема података броја TG. Подаци се чувају када гласовни QSO заврше (трајање > 5с).",
"mon_wait": "Чекање дневника...",
"sslog_login_ip": "Пријава по IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "ไม่มีรายการ OpenBridge จากเซิร์ฟเวอร์",
"opb_activity": "สตรีมที่ใช้งาน",
"opb_status": "สถานะ",
"opb_status_online": "ออนไลน์",
"opb_status_offline": "ออฟไลน์",
"toptg_no_data": "ยังไม่มีข้อมูลจำนวน TG ข้อมูลจะถูกบันทึกเมื่อ QSO เสียงสิ้นสุด (ระยะเวลา > 5 วินาที)",
"mon_wait": "กำลังรอบันทึก...",
"sslog_login_ip": "เข้าสู่ระบบด้วย IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Sunucudan OpenBridge girişi yok.",
"opb_activity": "Aktif akışlar",
"opb_status": "Durum",
"opb_status_online": "Çevrimiçi",
"opb_status_offline": "Çevrimdışı",
"toptg_no_data": "Henüz TG sayım verisi yok. Veriler ses QSO'ları bittiğinde (süre > 5sn) kaydedilir.",
"mon_wait": "Günlük bekleniyor...",
"sslog_login_ip": "IP ile giriş",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "Немає записів OpenBridge від сервера.",
"opb_activity": "Активні потоки",
"opb_status": "Статус",
"opb_status_online": "У мережі",
"opb_status_offline": "Поза мережею",
"toptg_no_data": "Ще немає даних підрахунку TG. Дані зберігаються після завершення голосових QSO (тривалість > 5с).",
"mon_wait": "Очікування журналу...",
"sslog_login_ip": "Увійти за IP",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"opb_title": "OpenBridge",
"opb_no_data": "服务器暂无 OpenBridge 条目。",
"opb_activity": "活动流",
"opb_status": "状态",
"opb_status_online": "在线",
"opb_status_offline": "离线",
"toptg_no_data": "暂无 TG 计数数据。语音 QSO 结束后(时长 > 5 秒)会保存数据。",
"mon_wait": "正在等待记录...",
"sslog_login_ip": "按 IP 登录",
Expand Down
67 changes: 61 additions & 6 deletions frontend/src/pages/OpenBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type OpenBridgeEntry = {
NETWORK_ID?: number | string;
TARGET_IP?: string;
TARGET_PORT?: string;
CONNECTED?: boolean;
STREAMS?: Record<string, StreamEntry>;
};
type Ctable = {
Expand All @@ -56,9 +57,30 @@ type Ctable = {
type OpbPayload = { ctable?: Ctable; dbridges?: boolean };

/** Suppress BCSQ fan-out flashes (START then END with Time: 0s in the same second). */
const OBP_CHIP_MIN_AGE_SEC = 0.1;
const OBP_CHIP_MIN_AGE_SEC = 0.5;
const OBP_CHIP_TICK_MS = 1000;

/** Country code from numeric network id (first 2-3 digits). */
function getCountryCodeFromId(id: number | string): string {
const n = String(id).replace(/\D/g, '');
if (n.length >= 3) return n.slice(0, 3);
if (n.length >= 2) return n.slice(0, 2);
return n || '';
}

function FlagImg({ code, fallback = 'world' }: { code: string; fallback?: string }) {
const src = code ? `/img/flags/${code}.png` : `/img/flags/${fallback}.png`;
return (
<Box
component="img"
src={src}
alt=""
onError={(e) => { (e.target as HTMLImageElement).src = `/img/flags/world.png`; }}
sx={{ width: 20, height: 14, objectFit: 'contain', flexShrink: 0 }}
/>
);
}

/** One visible row per (direction, callsign, TG); report may still carry duplicate stream_ids. */
function dedupeStreamRows(streams: Record<string, StreamEntry>) {
type Row = { id: string; dir: string; subCall: string; tg: string; timeout: number };
Expand Down Expand Up @@ -90,6 +112,29 @@ function visibleStreamRows(streams: Record<string, StreamEntry>, nowSec: number)
);
}

function ObpStatusDot({ connected, t }: { connected: boolean; t: (key: string, opts?: { defaultValue?: string }) => string }) {
const label = connected
? t('opb_status_online', { defaultValue: 'Online' })
: t('opb_status_offline', { defaultValue: 'Offline' });
const color = connected ? 'success.main' : 'error.main';
return (
<Box
component="span"
role="img"
aria-label={label}
title={label}
sx={{
display: 'inline-block',
width: 10,
height: 10,
borderRadius: '50%',
bgcolor: color,
flexShrink: 0,
}}
/>
);
}

export default function OpenBridge() {
const { t } = useTranslation();
const [nowSec, setNowSec] = useState(() => Date.now() / 1000);
Expand Down Expand Up @@ -128,15 +173,17 @@ export default function OpenBridge() {
<TableCaptionTitle>{t('opb_title', { defaultValue: 'OpenBridge' })}</TableCaptionTitle>
<TableHead>
<TableRow>
<TableCell sx={{ width: '18%', minWidth: 88, maxWidth: 140, whiteSpace: 'nowrap' }}>{t('lnksys_system', { defaultValue: 'OpenBridge' })}</TableCell>
<TableCell sx={{ width: '12%', minWidth: 72, maxWidth: 88, whiteSpace: 'nowrap' }}>{t('lnksys_network_id', { defaultValue: 'Network ID' })}</TableCell>
<TableCell sx={{ width: '18%', minWidth: 88, maxWidth: 140, whiteSpace: 'nowrap' }}>{t('lnksys_system', { defaultValue: 'Service' })}</TableCell>
<TableCell sx={{ width: '12%', minWidth: 88, maxWidth: 120, whiteSpace: 'nowrap' }}>{t('lnksys_network_id', { defaultValue: 'Network ID' })}</TableCell>
<TableCell sx={{ width: '70%', minWidth: 120 }}>{t('opb_activity', { defaultValue: 'Active streams' })}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(openbridges).map(([name, ob]) => {
const streams = ob?.STREAMS ?? {};
const streamList = visibleStreamRows(streams, nowSec);
const networkId = ob?.NETWORK_ID ?? '';
const flagCode = networkId !== '' ? getCountryCodeFromId(networkId) : '';
return (
<TableRow
key={name}
Expand All @@ -147,10 +194,18 @@ export default function OpenBridge() {
}}
>
<TableCell sx={{ width: '18%', minWidth: 88, maxWidth: 140, whiteSpace: 'nowrap' }}>
<Typography fontWeight="bold" component="span" noWrap>{name}</Typography>
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75, maxWidth: '100%' }}>
{typeof ob?.CONNECTED === 'boolean' && (
<ObpStatusDot connected={ob.CONNECTED} t={t} />
)}
<Typography fontWeight="bold" component="span" noWrap>{name}</Typography>
</Box>
</TableCell>
<TableCell sx={{ width: '12%', minWidth: 72, maxWidth: 88, whiteSpace: 'nowrap' }}>
<Typography fontWeight="bold" component="span" noWrap>{String(ob?.NETWORK_ID ?? '')}</Typography>
<TableCell sx={{ width: '12%', minWidth: 88, maxWidth: 120, whiteSpace: 'nowrap' }}>
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>
{networkId !== '' && <FlagImg code={flagCode} />}
<Typography fontWeight="bold" component="span" noWrap>{String(networkId)}</Typography>
</Box>
</TableCell>
<TableCell sx={{ width: '70%', overflow: 'auto', minWidth: 120 }}>
{/*
Expand Down
12 changes: 12 additions & 0 deletions monitor/src/adn_monitor/application/hblink_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ def build_hblink_table_impl(
stats_table["OPENBRIDGES"][name]["NETWORK_ID"] = int_id_fn(data.get("NETWORK_ID", 0))
# TARGET_IP/TARGET_PORT omitted on purpose: not sent over WebSocket, not shown in UI
stats_table["OPENBRIDGES"][name]["STREAMS"] = {}
_apply_openbridge_connected(stats_table["OPENBRIDGES"][name], data)


def _apply_openbridge_connected(obp_row: dict[str, Any], data: dict[str, Any]) -> None:
"""Sync ENHANCED OBP keepalive status from CONFIG into CTABLE (boolean CONNECTED)."""
if "OBP_CONNECTED" in data:
obp_row["CONNECTED"] = bool(data["OBP_CONNECTED"])
else:
obp_row.pop("CONNECTED", None)


def clean_te(stats_table: dict) -> None:
Expand Down Expand Up @@ -277,6 +286,9 @@ def update_hblink_table_impl(
"NETWORK_ID": int_id_fn(data.get("NETWORK_ID", 0)),
"STREAMS": {},
}
obp_row = stats_table["OPENBRIDGES"][name]
obp_row["NETWORK_ID"] = int_id_fn(data.get("NETWORK_ID", 0))
_apply_openbridge_connected(obp_row, data)
masters_table = stats_table.setdefault("MASTERS", {})
for name in config:
if config[name].get("MODE") != "MASTER":
Expand Down
2 changes: 2 additions & 0 deletions monitor/src/adn_monitor/application/report_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ def dashboard_state_to_config(doc: dict[str, Any], *, ts: float | None = None) -
entry["NETWORK_ID"] = _bytes_4(int(obp["network_id"]))
if obp.get("enhanced_obp"):
entry["ENHANCED_OBP"] = True
if isinstance(obp.get("connected"), bool):
entry["OBP_CONNECTED"] = obp["connected"]
config[str(name)] = entry

return config
Expand Down
5 changes: 4 additions & 1 deletion monitor/src/adn_monitor/application/ws_ctable_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ def opb_semantic_fingerprint(ctable: dict[str, Any], dbridges: bool) -> str:
if not isinstance(sys_ob, dict):
continue
streams = sys_ob.get("STREAMS") or {}
ob_out[sys_name] = {"STREAMS": _streams_semantic(streams)}
ob_entry: dict[str, Any] = {"STREAMS": _streams_semantic(streams)}
if isinstance(sys_ob.get("CONNECTED"), bool):
ob_entry["CONNECTED"] = sys_ob["CONNECTED"]
ob_out[sys_name] = ob_entry
return _fingerprint_dumps({"ctable": {"OPENBRIDGES": ob_out}, "dbridges": dbridges})


Expand Down
51 changes: 51 additions & 0 deletions monitor/tests/test_dashboard_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,57 @@ def test_dashboard_state_to_config_maps_linked_systems():
assert cfg["OBP-CL"]["ENHANCED_OBP"] is True


def test_dashboard_state_to_config_maps_obp_connected():
payload = {
"type": "dashboard_state",
"ts": 1.0,
"ctable": {
"MASTERS": {},
"PEERS": {},
"OPENBRIDGES": {
"OBP-CL": {
"mode": "OPENBRIDGE",
"network_id": 73010,
"enhanced_obp": True,
"connected": True,
"streams": {},
}
},
},
}
cfg = dashboard_state_to_config(payload)
assert cfg["OBP-CL"]["OBP_CONNECTED"] is True


def test_process_state_snd_applies_obp_connected():
state = MonitorState()
state.server_mode = ServerMode.V2
state.report_protocol = 2
deps = _deps()
deps["report_decoder"] = PickleJsonReportPayloadDecoder()
payload = {
"type": "dashboard_state",
"ts": 1.0,
"ctable": {
"MASTERS": {},
"PEERS": {},
"OPENBRIDGES": {
"OBP-CL": {
"mode": "OPENBRIDGE",
"network_id": 73010,
"enhanced_obp": True,
"connected": False,
"streams": {},
}
},
},
}
frame = Opcode.STATE_SND + json.dumps(payload, separators=(",", ":")).encode()
result = process_message(frame, state, **deps)
assert not is_fail(result)
assert state.CTABLE["OPENBRIDGES"]["OBP-CL"]["CONNECTED"] is False


def test_process_state_snd_applies_ctable():
state = MonitorState()
state.server_mode = ServerMode.V2
Expand Down
Loading
Loading