RSNewWebUI/webui-src/app/statusbar.js
defnax 33e7e867d7 Fixed issue to load chat images
* Added attach image button
* Improved status bar to retrieve nat status & down/up infos
* torstatus added but not yet functional needs the api
2026-07-25 16:09:00 +02:00

406 lines
15 KiB
JavaScript

const m = require('mithril');
const rs = require('rswebui');
// RS_HIDDEN_TYPE constants (from config_util.js / retroshare/rspeers.h)
const RS_HIDDEN_TYPE_NONE = 0;
const RS_HIDDEN_TYPE_TOR = 2;
const RS_HIDDEN_TYPE_I2P = 4;
const State = {
friendCount: 0,
onlineCount: 0,
dhtActive: false,
dhtOk: false,
dhtRsNetSize: 0,
dhtNetSize: 0,
natState: 1, // BAD_UNKNOWN
firewalled: true,
forwardPort: false,
stunOk: false,
extAddressOk: false,
// Hidden-mode / Tor+I2P state (mirrors TorStatus widget in Qt)
hiddenType: RS_HIDDEN_TYPE_NONE, // 0=none, 2=Tor, 4=I2P
torProxyOk: null, // null=unchecked, true=ok, false=fail
torChecking: false,
// Bandwidth rate status (mirrors RatesStatus widget in Qt)
rateIn: 0.0,
totalIn: 0,
rateOut: 0.0,
totalOut: 0,
};
function parse64Num(val) {
if (val === null || val === undefined) return 0;
if (typeof val === 'number') return val;
if (typeof val === 'string') return parseFloat(val) || 0;
if (typeof val === 'object') {
if (val.xuint64 !== undefined) return parseFloat(val.xuint64) || 0;
if (val.xint64 !== undefined) return parseFloat(val.xint64) || 0;
if (val.xstr64 !== undefined) return parseFloat(val.xstr64) || 0;
}
return 0;
}
function formatUnit(val) {
const num = parse64Num(val);
if (!num) return '0';
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
return num.toString();
}
function formatBytes(rawBytes) {
const bytes = parse64Num(rawBytes);
if (!bytes || bytes <= 0 || isNaN(bytes)) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const safeI = Math.max(0, Math.min(i, sizes.length - 1));
return parseFloat((bytes / Math.pow(k, safeI)).toFixed(1)) + ' ' + sizes[safeI];
}
/**
* Fetch the own peer's hidden type and proxy status using /rsTor API.
* Mirrors Qt's TorStatus::getTorStatus().
*/
function updateTorStatus() {
if (!rs.loginKey.isVerified) return;
rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => {
if (!res || !res.body || !res.body.retval) return;
const sslId = res.body.id;
rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }).then((pres) => {
if (!pres || !pres.body || !pres.body.retval) return;
const details = pres.body.det;
const isHiddenNode = Boolean(
details && (
details.hiddenType === RS_HIDDEN_TYPE_TOR ||
details.hiddenType === RS_HIDDEN_TYPE_I2P ||
details.extAddr === 'Hidden'
)
);
if (!isHiddenNode) {
State.hiddenType = RS_HIDDEN_TYPE_NONE;
State.torProxyOk = null;
State.torChecking = false;
m.redraw();
return;
}
const targetType = details.hiddenType || RS_HIDDEN_TYPE_TOR;
State.hiddenType = targetType;
// Check if node uses automated Tor management via /rsAccounts/isTorAuto (same as Qt)
rs.rsJsonApiRequest('/rsAccounts/isTorAuto', {}).then((autoRes) => {
const isAuto = autoRes && (autoRes.retval || (autoRes.body && autoRes.body.retval));
if (isAuto) {
Promise.all([
rs.rsJsonApiRequest('/rsTor/torStatus', {}),
rs.rsJsonApiRequest('/rsTor/torConnectivityStatus', {}),
]).then(([torRes, connRes]) => {
const torStatus = torRes && torRes.body ? torRes.body.retval : (torRes && torRes.retval !== undefined ? torRes.retval : 0);
const connStatus = connRes && connRes.body ? connRes.body.retval : (connRes && connRes.retval !== undefined ? connRes.retval : 0);
const torControlOk = connStatus === 6; // HIDDEN_SERVICE_READY
const torReady = torStatus === 2; // READY
if (torReady && torControlOk) {
State.torProxyOk = true;
State.torChecking = false;
} else if (torStatus === 1 || connStatus === 0 || connStatus === 1) {
// OFFLINE, ERROR, or NOT_CONNECTED
State.torProxyOk = false;
State.torChecking = false;
} else if (connStatus >= 2 && connStatus <= 5) {
// CONNECTING, SOCKET_CONNECTED, AUTHENTICATING, AUTHENTICATED
State.torProxyOk = null;
State.torChecking = true;
} else {
// UNKNOWN / default
State.torProxyOk = null;
State.torChecking = false;
}
m.redraw();
}).catch(() => {
State.torProxyOk = true;
State.torChecking = false;
m.redraw();
});
} else {
// Manual Tor / I2P proxy node
State.torProxyOk = true;
State.torChecking = false;
m.redraw();
}
}).catch(() => {
// Fallback for uncompiled backend: query /rsTor endpoints directly or assume active
Promise.all([
rs.rsJsonApiRequest('/rsTor/torStatus', {}),
rs.rsJsonApiRequest('/rsTor/torConnectivityStatus', {}),
]).then(([torRes, connRes]) => {
const torStatus = torRes && torRes.body ? torRes.body.retval : (torRes && torRes.retval !== undefined ? torRes.retval : 0);
const connStatus = connRes && connRes.body ? connRes.body.retval : (connRes && connRes.retval !== undefined ? connRes.retval : 0);
const torControlOk = connStatus === 6; // HIDDEN_SERVICE_READY
const torReady = torStatus === 2; // READY
if (torReady && torControlOk) {
State.torProxyOk = true;
State.torChecking = false;
} else if (torStatus === 1 || connStatus === 0 || connStatus === 1) {
State.torProxyOk = false;
State.torChecking = false;
} else if (connStatus >= 2 && connStatus <= 5) {
State.torProxyOk = null;
State.torChecking = true;
} else {
State.torProxyOk = null;
State.torChecking = false;
}
m.redraw();
}).catch(() => {
State.torProxyOk = true;
State.torChecking = false;
m.redraw();
});
});
});
}).catch(() => {
State.hiddenType = RS_HIDDEN_TYPE_NONE;
State.torProxyOk = null;
});
}
function updateStatus() {
if (!rs.loginKey.isVerified) return;
// 1. Friends count
rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => {
if (data && data.sslIds) {
State.friendCount = data.sslIds.length;
}
});
rs.rsJsonApiRequest('/rsPeers/getOnlineList', {}, (data) => {
if (data && data.sslIds) {
State.onlineCount = data.sslIds.length;
}
});
// 2. Net / DHT config status & NAT state
rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => {
if (data && data.status) {
State.dhtActive = data.status.DHTActive;
State.dhtOk = data.status.netDhtOk;
State.dhtRsNetSize = data.status.netDhtRsNetSize;
State.dhtNetSize = data.status.netDhtNetSize;
State.firewalled = data.status.firewalled;
State.forwardPort = data.status.forwardPort;
State.stunOk = data.status.netStunOk;
State.extAddressOk = data.status.netExtAddressOk;
// Compute NAT state directly from RsConfigNetStatus
if (!data.status.netLocalOk && !data.status.netExtAddressOk) {
State.natState = 2; // BAD_OFFLINE
} else if (data.status.firewalled && !data.status.forwardPort && !data.status.netUpnpOk) {
State.natState = 6; // WARNING_NATTED
} else if (data.status.forwardPort || data.status.netUpnpOk) {
State.natState = 9; // ADV_FORWARD
} else {
State.natState = 8; // GOOD
}
}
});
// 3. NAT netState from /rsConfig/getNetState
rs.rsJsonApiRequest('/rsConfig/getNetState', {}, (data) => {
if (data && data.retval !== undefined) {
State.natState = data.retval;
m.redraw();
} else if (data && data.body && data.body.retval !== undefined) {
State.natState = data.body.retval;
m.redraw();
}
});
// 4. Tor/I2P hidden-mode status (same as Qt TorStatus widget)
updateTorStatus();
// 5. Bandwidth rates (same as Qt RatesStatus widget)
rs.rsJsonApiRequest('/rsConfig/getTotalBandwidthRates', {}, (data) => {
const rates = (data && data.rates) || (data && data.body && data.body.rates);
if (rates) {
State.rateIn = rates.mRateIn !== undefined ? rates.mRateIn : (rates.rateIn || 0.0);
State.totalIn = rates.mTotalIn !== undefined ? rates.mTotalIn : (rates.totalIn || 0);
State.rateOut = rates.mRateOut !== undefined ? rates.mRateOut : (rates.rateOut || 0.0);
State.totalOut = rates.mTotalOut !== undefined ? rates.mTotalOut : (rates.totalOut || 0);
m.redraw();
}
});
}
let intervalId = null;
const StatusBar = {
oninit() {
updateStatus();
intervalId = setInterval(updateStatus, 10000); // update every 10s
},
onremove() {
if (intervalId) {
clearInterval(intervalId);
}
},
view() {
const isHiddenMode = State.hiddenType === RS_HIDDEN_TYPE_TOR ||
State.hiddenType === RS_HIDDEN_TYPE_I2P;
// ── DHT Status (hidden when in hidden/darknet mode) ────────────────────
let dhtColor = '#94a3b8'; // grey (off)
let dhtTooltip = 'DHT Off';
if (State.dhtActive) {
if (State.dhtOk) {
if (State.dhtRsNetSize < 10) {
dhtColor = '#eab308'; // yellow (searching)
dhtTooltip = 'DHT Searching for RetroShare Peers';
} else {
dhtColor = '#22c55e'; // green (good)
dhtTooltip = 'DHT Good';
}
} else {
dhtColor = '#ef4444'; // red (error)
dhtTooltip = 'No peer found in DHT';
}
}
// ── NAT Status (hidden when in hidden/darknet mode) ────────────────────
let natColor = '#94a3b8';
let natTooltip = 'Offline';
switch (State.natState) {
case 1: // BAD_UNKNOWN
natColor = '#eab308';
natTooltip = 'Network Status Unknown';
break;
case 2: // BAD_OFFLINE
natColor = '#94a3b8';
natTooltip = 'Offline';
break;
case 3: // BAD_NATSYM
case 4: // BAD_NODHT_NAT
natColor = '#ef4444';
natTooltip = State.natState === 4 ? 'DHT Disabled and Firewalled' : 'Nasty Firewall';
break;
case 5: // WARNING_RESTART
natColor = '#eab308';
natTooltip = 'Network Restarting';
break;
case 6: // WARNING_NATTED
natColor = '#eab308';
natTooltip = 'Behind Firewall';
break;
case 7: // WARNING_NODHT
natColor = '#eab308';
natTooltip = 'DHT Disabled';
break;
case 8: // GOOD
natColor = '#22c55e';
natTooltip = 'RetroShare Server';
break;
case 9: // ADV_FORWARD
natColor = '#22c55e';
natTooltip = 'Forwarded Port';
break;
}
// ── Tor / I2P status indicator ─────────────────────────────────────────
// Only shown when peer is in RS_NETMODE_HIDDEN with a proxy type set.
// Mirrors Qt TorStatus widget label + icon logic.
let torLabel, torColor, torIcon, torTooltip;
if (isHiddenMode) {
torLabel = State.hiddenType === RS_HIDDEN_TYPE_TOR ? 'Tor:' : 'I2P:';
if (State.torChecking) {
torColor = '#f59e0b';
torIcon = 'fas fa-spinner fa-spin';
torTooltip = 'Checking proxy…';
} else if (State.torProxyOk === null) {
torColor = '#94a3b8';
torIcon = 'fas fa-shield-alt';
torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR
? 'No Tor configuration'
: 'No I2P configuration';
} else if (State.torProxyOk) {
torColor = '#22c55e';
torIcon = 'fas fa-shield-alt';
torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR
? 'Tor proxy is OK'
: 'I2P proxy is OK';
} else {
torColor = '#ef4444';
torIcon = 'fas fa-shield-alt';
torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR
? 'Tor proxy is not available'
: 'I2P proxy is not available';
}
}
return m('.statusbar', [
m('.statusbar-left', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [
m('.statusbar-item', [
m('i.fas.fa-users', { style: 'margin-right: 0.5rem; color: #94a3b8;' }),
m('span', `Friends: ${State.onlineCount}/${State.friendCount}`),
]),
// NAT — hidden when in hidden/darknet mode (same as Qt)
!isHiddenMode && m('.statusbar-divider'),
!isHiddenMode && m('.statusbar-item', { title: natTooltip, style: 'cursor: help;' }, [
m('span', { style: 'margin-right: 0.5rem;' }, 'NAT:'),
m('.status-bullet', { style: { backgroundColor: natColor } }),
]),
// DHT — hidden when in hidden/darknet mode (same as Qt)
!isHiddenMode && m('.statusbar-divider'),
!isHiddenMode && m('.statusbar-item', { title: dhtTooltip, style: 'cursor: help;' }, [
m('span', { style: 'margin-right: 0.5rem;' }, 'DHT:'),
m('.status-bullet', { style: { backgroundColor: dhtColor } }),
State.dhtActive && State.dhtOk && m('span', { style: 'margin-left: 0.5rem;' }, `${formatUnit(State.dhtRsNetSize)} (${formatUnit(State.dhtNetSize)})`),
]),
// Tor / I2P — only shown when in hidden/darknet mode (same as Qt)
isHiddenMode && m('.statusbar-divider'),
isHiddenMode && m('.statusbar-item.statusbar-item--tor', {
title: torTooltip,
style: 'cursor: help;',
}, [
m('span.tor-label', { style: 'margin-right: 0.4rem; font-weight: 600;' }, torLabel),
m('i.' + torIcon, { style: { color: torColor, fontSize: '1rem', transition: 'color 0.3s' } }),
]),
]),
// RatesStatus — Bandwidth speeds & total cumulative transfer (Down | Up)
m('.statusbar-right', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [
m('.statusbar-item', {
title: `Downloaded: ${formatBytes(State.totalIn)}`,
style: 'cursor: help; display: flex; align-items: center;'
}, [
m('i.fas.fa-arrow-down', { style: 'color: #22c55e; margin-right: 0.35rem;' }),
m('span', `Down: ${State.rateIn.toFixed(2)} kB/s`),
m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalIn)})`),
]),
m('.statusbar-divider'),
m('.statusbar-item', {
title: `Uploaded: ${formatBytes(State.totalOut)}`,
style: 'cursor: help; display: flex; align-items: center;'
}, [
m('i.fas.fa-arrow-up', { style: 'color: #3b82f6; margin-right: 0.35rem;' }),
m('span', `Up: ${State.rateOut.toFixed(2)} kB/s`),
m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalOut)})`),
]),
]),
]);
},
};
module.exports = StatusBar;