3 Commits
Author SHA1 Message Date
ariska 813d3aa06e secure chat addon and stun 2026-07-02 09:12:57 +07:00
ariska a45fa67ed2 chat addon 2026-06-30 13:44:11 +07:00
ariska d604c01114 chat 2026-06-30 13:10:35 +07:00
164 changed files with 16357 additions and 2169 deletions

No files matched your search

@@ -0,0 +1,10 @@
{
"id": "comms-core",
"name": "Comms Core",
"version": "1.17.1",
"requires": [],
"backend": {
"routes": ["/controller/comms"],
"websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
}
}
+4
View File
@@ -27,6 +27,10 @@ ifeq ($(OSTYPE),NetBSD)
include make-netbsd.mk include make-netbsd.mk
endif endif
ifeq ($(OS),Windows_NT)
include make-windows.mk
endif
drone: drone:
@echo "rendering .drone.yaml from .drone.jsonnet" @echo "rendering .drone.yaml from .drone.jsonnet"
drone jsonnet --format --stream drone jsonnet --format --stream
Binary file not shown.
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
window.BackOneRingingPopup = {
_audio: null,
showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
const el = document.getElementById('ringing-popup');
if (!el) return;
el.classList.remove('ringing-hidden');
el.innerHTML = `
<div class="ringing-card">
<h2>Incoming ${callType} call</h2>
<p>${callerName || 'Unknown peer'}</p>
<button class="ringing-accept">Accept</button>
<button class="ringing-reject">Reject</button>
</div>`;
el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
this.playRingtone();
if (Notification.permission === 'default') Notification.requestPermission();
if (Notification.permission === 'granted') {
new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
}
},
dismiss() {
const el = document.getElementById('ringing-popup');
if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
this.stopRingtone();
},
playRingtone() {
this.stopRingtone();
try {
this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
this._audio.loop = true;
this._audio.play().catch(() => {});
} catch (_) {}
},
stopRingtone() {
if (this._audio) { this._audio.pause(); this._audio = null; }
}
};
async function loadAddonManifest() {
const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
if (!res.ok) {
console.warn('manifest.json fetch failed:', res.status);
return [];
}
const data = await res.json();
return Array.isArray(data) ? data : [];
}
async function loadAddonModule(entry) {
const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
await import(entryPath);
const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
return factory ? factory() : null;
}
window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
+195
View File
@@ -0,0 +1,195 @@
/**
* BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
*/
window.BackOneAuth = (function () {
const LS_KEY = 'backone.authToken';
let cache = null;
function trimToken(s) {
return String(s || '').replace(/\s+/g, '');
}
function tokenFromUrl() {
try {
const params = new URLSearchParams(location.search);
const fromQuery = trimToken(params.get('auth') || params.get('token'));
if (fromQuery) return fromQuery;
const hash = (location.hash || '').replace(/^#\/?/, '');
const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
return trimToken(hashParams.get('auth') || hashParams.get('token'));
} catch (_) {
return '';
}
}
async function probeToken(token) {
if (!token) return false;
try {
const res = await fetch('/network', {
cache: 'no-store',
headers: { 'X-ZT1-Auth': token }
});
return res.ok;
} catch (_) {
return false;
}
}
async function fetchAuthtoken() {
for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
try {
const res = await fetch(path, { cache: 'no-store' });
if (res.ok) {
const data = await res.json();
const token = trimToken(data && data.token);
if (token) return token;
}
} catch (_) {
/* try next */
}
}
return '';
}
async function resolveToken() {
const urlToken = tokenFromUrl();
if (urlToken) {
if (await probeToken(urlToken)) {
return urlToken;
}
}
const stored = trimToken(localStorage.getItem(LS_KEY));
if (stored && await probeToken(stored)) {
return stored;
}
const apiToken = await fetchAuthtoken();
if (apiToken && await probeToken(apiToken)) {
return apiToken;
}
if (stored) return stored;
if (apiToken) return apiToken;
if (urlToken) return urlToken;
return '';
}
async function getToken() {
if (cache) return cache;
cache = await resolveToken();
return cache || '';
}
async function authHeaders(extra) {
const token = await getToken();
const headers = Object.assign({}, extra || {});
if (token) {
headers['X-ZT1-Auth'] = token;
}
return headers;
}
async function fetchWithAuth(path, options) {
options = options || {};
const headers = await authHeaders(options.headers || {});
return fetch(path, Object.assign({}, options, { headers }));
}
async function apiGet(path) {
const res = await fetchWithAuth(path, { cache: 'no-store' });
if (!res.ok) throw new Error(`${path} returned ${res.status}`);
return res.json();
}
async function apiGetFirst(paths) {
let lastErr = null;
for (const path of paths) {
try {
return await apiGet(path);
} catch (err) {
lastErr = err;
}
}
throw lastErr || new Error('API request failed');
}
async function apiSend(path, method, body) {
const headers = await authHeaders(
body != null ? { 'Content-Type': 'application/json' } : {}
);
const res = await fetch(path, {
method,
headers,
body: body != null ? JSON.stringify(body) : undefined
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(text || `${method} ${path} returned ${res.status}`);
}
if (res.status === 204) {
return { result: true };
}
const text = await res.text();
if (!text) {
return { result: true };
}
try {
return JSON.parse(text);
} catch (_) {
return { result: true };
}
}
async function apiSendFirst(paths, method, body) {
let lastErr = null;
for (const path of paths) {
try {
return await apiSend(path, method, body);
} catch (err) {
lastErr = err;
}
}
throw lastErr || new Error(`${method} failed`);
}
/** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
async function setUserToken(token) {
cache = trimToken(token);
if (cache) {
localStorage.setItem(LS_KEY, cache);
} else {
localStorage.removeItem(LS_KEY);
}
return cache;
}
async function validateCurrentToken() {
cache = null;
const token = await getToken();
if (!token) return false;
return probeToken(token);
}
function invalidate() {
cache = null;
}
async function init() {
return getToken();
}
return {
getToken,
authHeaders,
fetchWithAuth,
apiGet,
apiGetFirst,
apiSend,
apiSendFirst,
setUserToken,
validateCurrentToken,
invalidate,
init
};
})();
+57
View File
@@ -0,0 +1,57 @@
window.BackOneContacts = (function () {
let cache = null;
async function load() {
if (cache) return cache;
try {
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
.catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
if (!res.ok) {
cache = {};
return cache;
}
cache = await res.json();
if (!cache || typeof cache !== 'object') cache = {};
} catch (_) {
cache = {};
}
return cache;
}
function getDisplayName(peerId, controllerName) {
const c = cache || {};
const entry = c[peerId];
if (entry && entry.nickname) return entry.nickname;
if (controllerName) return controllerName;
return peerId;
}
function getNickname(peerId) {
const entry = (cache || {})[peerId];
return entry && entry.nickname ? entry.nickname : '';
}
async function setNickname(peerId, nickname) {
await load();
const patch = {};
patch[peerId] = { nickname: nickname || '' };
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}));
if (!res.ok) throw new Error('Failed to save nickname');
cache = await res.json();
return cache;
}
function invalidate() {
cache = null;
}
return { load, getDisplayName, getNickname, setNickname, invalidate };
})();
+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BackOne</title>
<link rel="stylesheet" href="/app/styles.css">
</head>
<body>
<header>
<h1>BackOne</h1>
<div id="network-bar" class="network-bar">
<label class="network-bar-label">Network
<select id="global-network"></select>
</label>
<input id="global-join-id" type="text" maxlength="16" placeholder="Join network ID" autocomplete="off" />
<button type="button" id="global-join-btn">Join</button>
<button type="button" id="global-leave-btn">Leave</button>
<span id="global-network-status" class="network-bar-status"></span>
<div id="auth-token-bar" class="auth-token-bar auth-token-hidden">
<label class="auth-token-label">Auth token
<input id="auth-token-input" type="password" autocomplete="off"
placeholder="Paste from ~/.backOneOneAuthToken" />
</label>
<button type="button" id="auth-token-save">Save</button>
</div>
</div>
<nav id="addon-nav"></nav>
</header>
<main id="addon-panel">
<p>Select an addon from the navigation bar.</p>
</main>
<div id="ringing-popup" class="ringing-hidden"></div>
<script src="/app/auth-store.js"></script>
<script src="/app/contacts-store.js"></script>
<script src="/app/peer-directory.js"></script>
<script src="/app/network-session.js"></script>
<script src="/app/peer-picker.js"></script>
<script src="/app/addon-loader.js"></script>
<script src="/app/main.js"></script>
</body>
</html>
+228
View File
@@ -0,0 +1,228 @@
window.BackOneAddons = window.BackOneAddons || {};
const addonButtons = {};
let addonModules = {};
let coreRefreshTimer = null;
function hashAddonId() {
const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
if (h === 'voice' || h === 'video' || h === 'chat') return h;
return '';
}
function activateAddon(id) {
const btn = addonButtons[id];
if (!btn) return;
btn.click();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function renderCoreStatus(statusEl) {
const core = BackOneNetworkSession.getCoreStatus();
const net = BackOneNetworkSession.getActiveNetwork();
if (!core.online) {
statusEl.innerHTML = '<span class="core-dot offline"></span> BackOne offline — start <code>backone</code> service';
return;
}
const netPart = net ? networkLabel(net) : 'no network joined';
const addr = core.address ? ` · ${escapeHtml(core.address)}` : '';
const authNote = core.authenticated
? ' · auth token OK'
: ' · <span class="peer-network-warn">paste <code>~/.backOneOneAuthToken</code> below</span>';
statusEl.innerHTML =
`<span class="core-dot online"></span> BackOne online${addr} · ` +
`<strong>${escapeHtml(netPart)}</strong> · ${core.leafPeerCount} peer(s)${authNote}`;
}
async function initAuthTokenBar(statusEl, onSaved) {
const bar = document.getElementById('auth-token-bar');
const input = document.getElementById('auth-token-input');
const saveBtn = document.getElementById('auth-token-save');
if (!bar || !input || !saveBtn) return;
async function refreshAuthUi() {
const ok = await BackOneAuth.validateCurrentToken();
bar.classList.toggle('auth-token-hidden', ok);
if (!ok && !input.value) {
input.placeholder = 'Paste from ~/.backOneOneAuthToken';
}
return ok;
}
saveBtn.addEventListener('click', async () => {
const token = input.value.trim();
if (!token) {
if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
return;
}
saveBtn.disabled = true;
await BackOneAuth.setUserToken(token);
BackOneAuth.invalidate();
const ok = await BackOneAuth.validateCurrentToken();
if (!ok) {
if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken matches the running backone service';
saveBtn.disabled = false;
return;
}
input.value = '';
bar.classList.add('auth-token-hidden');
if (typeof onSaved === 'function') await onSaved();
saveBtn.disabled = false;
});
await refreshAuthUi();
return refreshAuthUi;
}
async function initNetworkBar() {
const bar = document.getElementById('network-bar');
if (!bar) return;
const netSel = bar.querySelector('#global-network');
const joinInput = bar.querySelector('#global-join-id');
const joinBtn = bar.querySelector('#global-join-btn');
const leaveBtn = bar.querySelector('#global-leave-btn');
const statusEl = bar.querySelector('#global-network-status');
async function reloadNetworkList(selectId) {
await BackOneNetworkSession.syncFromCore();
const networks = await BackOneNetworkSession.loadNetworks();
netSel.innerHTML = networks.map(n =>
`<option value="${n.id}">${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || '')}</option>`
).join('') || '<option value="">— no networks joined —</option>';
const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
await BackOneNetworkSession.selectNetwork(netSel.value);
} else if (networks.length === 1) {
netSel.value = networks[0].id;
await BackOneNetworkSession.selectNetwork(networks[0].id);
} else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
netSel.value = BackOneNetworkSession.getNetworkId();
}
renderCoreStatus(statusEl);
}
netSel.addEventListener('change', async () => {
if (!netSel.value) return;
statusEl.textContent = 'Switching network…';
await BackOneNetworkSession.selectNetwork(netSel.value);
await BackOneNetworkSession.syncFromCore();
renderCoreStatus(statusEl);
});
joinBtn.addEventListener('click', async () => {
const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
if (!id) {
statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
return;
}
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Joining ${id}…`;
try {
await BackOneNetworkSession.joinNetwork(id);
joinInput.value = '';
await reloadNetworkList(id);
} catch (err) {
statusEl.textContent = err.message || 'Join failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
leaveBtn.addEventListener('click', async () => {
const id = netSel.value || BackOneNetworkSession.getNetworkId();
if (!id) {
statusEl.textContent = 'Select a network to leave';
return;
}
if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Leaving ${id}…`;
try {
await BackOneNetworkSession.leaveNetwork(id);
await reloadNetworkList();
} catch (err) {
statusEl.textContent = err.message || 'Leave failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
BackOneNetworkSession.subscribe(() => renderCoreStatus(statusEl));
const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
await reloadNetworkList();
if (refreshAuthUi) await refreshAuthUi();
coreRefreshTimer = setInterval(() => {
BackOneNetworkSession.syncFromCore().then(() => renderCoreStatus(statusEl));
}, 45000);
}
async function initAppShell() {
await BackOneAuth.init();
await initNetworkBar();
const nav = document.getElementById('addon-nav');
const panel = document.getElementById('addon-panel');
const addons = await BackOneAddonLoader.loadAddonManifest();
if (!addons.length) {
panel.innerHTML = '<p>No UI addons found. Install backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: <code>sudo backone-updater rebuild-manifest</code> and restart backone.</p>';
return;
}
for (const entry of addons) {
const mod = await BackOneAddonLoader.loadAddonModule(entry);
if (mod) addonModules[entry.id] = mod;
const btn = document.createElement('button');
btn.dataset.addonId = entry.id;
btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
btn.onclick = () => {
nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
panel.innerHTML = '';
if (addonModules[entry.id] && addonModules[entry.id].mount) {
addonModules[entry.id].mount(panel);
} else {
panel.innerHTML = `<p>Addon "${entry.id}" loaded.</p>`;
}
};
addonButtons[entry.id] = btn;
nav.appendChild(btn);
}
const fromHash = hashAddonId();
if (fromHash && addonButtons[fromHash]) {
activateAddon(fromHash);
} else {
nav.querySelector('button')?.click();
}
}
window.addEventListener('hashchange', () => {
const id = hashAddonId();
if (id) activateAddon(id);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
BackOneNetworkSession.syncFromCore();
}
});
document.addEventListener('DOMContentLoaded', initAppShell);
+178
View File
@@ -0,0 +1,178 @@
/**
* Single BackOne core network for all addons (same as backone-cli listnetworks/join).
*/
window.BackOneNetworkSession = (function () {
const NETWORK_KEY = 'backone.lastNetworkId';
const PEER_KEY = 'backone.lastPeerId';
let activeNetwork = null;
let selectedPeer = null;
let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
const listeners = new Set();
function emit() {
const snap = {
network: activeNetwork,
networkId: getNetworkId(),
peer: selectedPeer,
core: { ...coreStatus }
};
listeners.forEach(fn => {
try { fn(snap); } catch (_) { /* ignore */ }
});
}
function getNetworkId() {
return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
}
function setNetworkId(id) {
const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
if (nwid) {
localStorage.setItem(NETWORK_KEY, nwid);
} else {
localStorage.removeItem(NETWORK_KEY);
}
BackOnePeerDirectory.setLastNetworkId(nwid);
return nwid;
}
function getSelectedPeerId() {
return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
}
function setSelectedPeer(peer) {
selectedPeer = peer || null;
if (peer && peer.peerId) {
localStorage.setItem(PEER_KEY, peer.peerId);
} else {
localStorage.removeItem(PEER_KEY);
}
emit();
}
function pickDefaultNetworkId(networks) {
if (!networks.length) return '';
const stored = getNetworkId();
if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
return stored;
}
const preferred = networks.find(n => n.status === 'OK')
|| networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
|| networks[0];
return preferred ? (preferred.id || '').toLowerCase() : '';
}
/**
* On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
*/
async function syncFromCore() {
const token = await BackOneAuth.getToken();
const [status, networks, cliPeers] = await Promise.all([
BackOnePeerDirectory.loadCoreStatus(),
BackOnePeerDirectory.loadNetworks(),
BackOnePeerDirectory.loadCliPeers()
]);
coreStatus = {
online: !!(status && status.online),
address: (status && status.address) || '',
version: (status && status.version) || '',
networkCount: networks.length,
leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
authenticated: !!token
};
const pick = pickDefaultNetworkId(networks);
if (pick) {
setNetworkId(pick);
activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
|| await BackOnePeerDirectory.loadNetwork(pick);
} else {
setNetworkId('');
activeNetwork = null;
}
emit();
return { coreStatus, activeNetwork, networks };
}
async function refresh() {
const id = getNetworkId();
if (!id) {
activeNetwork = null;
emit();
return null;
}
activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
if (!activeNetwork) {
activeNetwork = { id, name: id, status: 'UNKNOWN' };
}
emit();
return activeNetwork;
}
async function loadNetworks() {
return BackOnePeerDirectory.loadNetworks();
}
async function selectNetwork(nwid) {
const id = setNetworkId(nwid);
if (!id) {
await refresh();
return null;
}
return refresh();
}
async function joinNetwork(nwid) {
const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
await BackOnePeerDirectory.joinNetwork(id);
return syncFromCore();
}
async function leaveNetwork(nwid) {
const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
if (!id) throw new Error('No network selected');
await BackOnePeerDirectory.leaveNetwork(id);
if (getNetworkId() === id) {
setNetworkId('');
selectedPeer = null;
localStorage.removeItem(PEER_KEY);
}
return syncFromCore();
}
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function requireNetworkId() {
const id = getNetworkId();
if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
return id;
}
function getCoreStatus() {
return { ...coreStatus };
}
return {
getNetworkId,
getActiveNetwork: () => activeNetwork,
getCoreStatus,
getSelectedPeer: () => selectedPeer,
getSelectedPeerId,
setSelectedPeer,
selectNetwork,
joinNetwork,
leaveNetwork,
loadNetworks,
syncFromCore,
refresh,
subscribe,
requireNetworkId
};
})();
+266
View File
@@ -0,0 +1,266 @@
window.BackOnePeerDirectory = (function () {
const NETWORK_KEY = 'backone.lastNetworkId';
const API = {
peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
networks: ['/api/backone/networks', '/app/api/networks', '/network'],
status: ['/api/backone/status', '/app/api/status', '/status'],
network: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
],
members: (id) => [
`/api/backone/network/${id}/members`,
`/app/api/network/${id}/members`,
`/unstable/controller/network/${id}/member`
],
join: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
],
leave: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
]
};
function apiGet(path) {
return BackOneAuth.apiGet(path);
}
/** Same data as `backone-cli -j peers` (GET /peer). */
async function loadCliPeers() {
try {
const peers = await BackOneAuth.apiGetFirst(API.peers);
return Array.isArray(peers) ? peers : [];
} catch (_) {
return [];
}
}
/** Same data as `backone-cli -j listnetworks` (GET /network). */
async function loadNetworks() {
try {
const nets = await BackOneAuth.apiGetFirst(API.networks);
return Array.isArray(nets) ? nets : [];
} catch (_) {
return [];
}
}
/** Same data as `backone-cli -j info` (GET /status). */
async function loadCoreStatus() {
try {
return await BackOneAuth.apiGetFirst(API.status);
} catch (_) {
try {
const res = await fetch('/health', { cache: 'no-store' });
return { online: res.ok };
} catch (_2) {
return { online: false };
}
}
}
function countLeafPeers(cliPeers) {
if (!Array.isArray(cliPeers)) return 0;
return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
}
async function loadNetwork(nwid) {
if (!nwid) return null;
const networks = await loadNetworks();
const found = networks.find(n => n.id === nwid);
if (found) return found;
try {
return await BackOneAuth.apiGetFirst(API.network(nwid));
} catch (_) {
return null;
}
}
function getLastNetworkId() {
return localStorage.getItem(NETWORK_KEY) || '';
}
function setLastNetworkId(nwid) {
if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
}
function peerIsOnline(p) {
if (!p) return false;
const paths = p.paths || [];
return paths.some(x => x.active && !x.expired);
}
function peerLinkLabel(p) {
if (!p) return '';
if (p.tunneled) return 'RELAY';
const paths = p.paths || [];
for (const path of paths) {
if (path.preferred && path.active && !path.expired) {
return 'DIRECT';
}
}
for (const path of paths) {
if (path.active && !path.expired) {
return 'DIRECT';
}
}
return 'RELAY';
}
function peerPreferredPath(p) {
if (!p) return '';
const paths = p.paths || [];
for (const path of paths) {
if (path.preferred && path.address) return path.address;
}
for (const path of paths) {
if (path.active && path.address) return path.address;
}
return '';
}
function cliPeerMap(cliPeers) {
const map = {};
for (const p of cliPeers) {
const id = (p.address || '').toLowerCase();
if (id) map[id] = p;
}
return map;
}
async function loadMemberMap(nwid) {
if (!nwid) return {};
try {
const data = await BackOneAuth.apiGetFirst(API.members(nwid));
const map = {};
if (data && Array.isArray(data.data)) {
for (const m of data.data) {
const id = (m.id || '').toLowerCase();
if (id) map[id] = m;
}
}
return map;
} catch (_) {
return {};
}
}
function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
const controllerName = member ? (member.name || '') : '';
const ips = member && member.ipAssignments ? member.ipAssignments : [];
return {
peerId,
networkId: networkId || '',
networkName: networkName || '',
displayName: BackOneContacts.getDisplayName(peerId, controllerName),
controllerName,
nickname: BackOneContacts.getNickname(peerId),
online: peerIsOnline(cliPeer),
latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
version: (cliPeer && cliPeer.version) || '',
link: cliPeer ? peerLinkLabel(cliPeer) : '',
path: cliPeer ? peerPreferredPath(cliPeer) : '',
ip: ips.length ? ips[0] : '',
onNetwork: !!member
};
}
async function loadPeersForNetwork(nwid) {
if (!nwid) return [];
const [cliPeers, members, selfStatus, network] = await Promise.all([
loadCliPeers(),
loadMemberMap(nwid),
loadCoreStatus().catch(() => ({})),
loadNetwork(nwid)
]);
await BackOneContacts.load();
const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
const networkId = nwid;
const networkName = network ? (network.name || network.id || nwid) : nwid;
const cliById = cliPeerMap(cliPeers);
const out = [];
if (Object.keys(members).length > 0) {
for (const peerId of Object.keys(members)) {
const member = members[peerId];
if (!member || member.authorized === false) continue;
if (peerId === selfId) continue;
out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
}
} else {
for (const peerId of Object.keys(cliById)) {
const p = cliById[peerId];
if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
if (p.role && p.role !== 'LEAF') continue;
out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
}
}
out.sort((a, b) => {
if (a.online !== b.online) return a.online ? -1 : 1;
return a.displayName.localeCompare(b.displayName);
});
return out;
}
function searchPeers(query, list) {
const q = (query || '').trim().toLowerCase();
if (!q) return list;
return list.filter(p =>
p.displayName.toLowerCase().includes(q) ||
p.peerId.includes(q) ||
(p.networkName || '').toLowerCase().includes(q) ||
(p.networkId || '').includes(q) ||
(p.controllerName || '').toLowerCase().includes(q) ||
(p.role || '').toLowerCase().includes(q) ||
(p.path || '').toLowerCase().includes(q) ||
(p.ip || '').toLowerCase().includes(q)
);
}
function normalizeNetworkId(input) {
const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
return id.length === 16 ? id : '';
}
async function joinNetwork(nwid) {
const id = normalizeNetworkId(nwid);
if (!id) {
throw new Error('Network ID must be 16 hexadecimal characters');
}
return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
}
async function leaveNetwork(nwid) {
const id = normalizeNetworkId(nwid);
if (!id) {
throw new Error('Network ID must be 16 hexadecimal characters');
}
return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
}
return {
loadCliPeers,
loadNetworks,
loadNetwork,
loadCoreStatus,
countLeafPeers,
loadPeersForNetwork,
getLastNetworkId,
setLastNetworkId,
joinNetwork,
leaveNetwork,
normalizeNetworkId,
searchPeers,
peerIsOnline,
peerLinkLabel
};
})();
+151
View File
@@ -0,0 +1,151 @@
window.BackOnePeerPicker = function PeerPicker(container, options) {
options = options || {};
const onSelect = options.onSelect || (() => {});
const onNetworkChange = options.onNetworkChange || (() => {});
const onAction = options.onAction || null;
const actionLabel = options.actionLabel || 'Select';
const showActionButton = options.showActionButton !== false;
const useSharedNetwork = options.useSharedNetwork !== false;
let selectedPeer = null;
let peers = [];
let currentNetwork = null;
let unsub = null;
container.innerHTML = `
<div class="peer-picker">
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
<div id="pp-network-banner" class="peer-network-banner"></div>
<label class="peer-picker-row">Search
<input id="pp-search" type="search" placeholder="Name or peer ID" />
</label>
<ul id="pp-list" class="peer-list"></ul>
<div id="pp-selected" class="peer-selected"></div>
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
</div>`;
const netBanner = container.querySelector('#pp-network-banner');
const searchInput = container.querySelector('#pp-search');
const listEl = container.querySelector('#pp-list');
const selectedEl = container.querySelector('#pp-selected');
const actionBtn = container.querySelector('#pp-action');
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function renderNetworkBanner() {
if (!useSharedNetwork) return;
const id = BackOneNetworkSession.getNetworkId();
const core = BackOneNetworkSession.getCoreStatus();
if (!core.online) {
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers from <code>backone-cli</code> unavailable</span>';
return;
}
if (!currentNetwork || !id) {
netBanner.innerHTML = '<span class="peer-network-warn">No network — core reports ' + core.networkCount + ' joined (use header bar or <code>backone-cli listnetworks</code>)</span>';
return;
}
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
}
function renderList() {
const q = searchInput.value;
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
const netTag = currentNetwork ? escapeHtml(currentNetwork.name || currentNetwork.id) : '';
listEl.innerHTML = filtered.map(p => {
const lat = p.latency != null ? `${p.latency}ms` : '—';
const meta = [p.role || 'LEAF', lat, p.link || ''].filter(Boolean).join(' · ');
const ip = p.ip ? ` · ${escapeHtml(p.ip)}` : '';
return `
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
<span class="peer-main">
<strong class="peer-name">${escapeHtml(p.displayName)}</strong>
<small class="peer-sub">${escapeHtml(p.peerId)} · ${escapeHtml(meta)}${netTag ? ' · ' + netTag : ''}${ip}</small>
</span>
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
</li>`;
}).join('') || '<li class="peer-empty">No peers on the active network — check <code>backone-cli peers</code> and controller members</li>';
listEl.querySelectorAll('.peer-item').forEach(li => {
li.addEventListener('click', (ev) => {
if (ev.target.classList.contains('peer-edit')) return;
const id = li.dataset.id;
selectedPeer = peers.find(x => x.peerId === id) || null;
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
renderList();
renderSelected();
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
});
const editBtn = li.querySelector('.peer-edit');
if (editBtn) {
editBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const id = li.dataset.id;
const peer = peers.find(x => x.peerId === id);
if (!peer) return;
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
if (nick === null) return;
await BackOneContacts.setNickname(peer.peerId, nick);
BackOneContacts.invalidate();
await refreshPeers();
});
}
});
}
function renderSelected() {
if (!selectedPeer) {
selectedEl.textContent = '';
if (actionBtn) actionBtn.disabled = true;
return;
}
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function refreshPeers() {
currentNetwork = BackOneNetworkSession.getActiveNetwork();
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
currentNetwork = await BackOneNetworkSession.refresh();
}
renderNetworkBanner();
const nwid = BackOneNetworkSession.getNetworkId();
await BackOneContacts.load();
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
if (lastPeerId) {
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
}
renderList();
renderSelected();
onNetworkChange(currentNetwork, peers);
}
searchInput.addEventListener('input', renderList);
if (actionBtn) {
actionBtn.addEventListener('click', () => {
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
});
}
if (useSharedNetwork) {
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
}
refreshPeers();
return {
getSelectedPeer: () => selectedPeer,
getSelectedNetwork: () => currentNetwork,
refresh: refreshPeers,
destroy: () => { if (unsub) unsub(); }
};
};
+53
View File
@@ -0,0 +1,53 @@
body { font-family: system-ui, sans-serif; margin: 0; background: #0f1419; color: #e7ecf1; }
header { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #1a2332; border-bottom: 1px solid #2d3a4d; flex-wrap: wrap; }
header h1 { margin: 0; font-size: 1.25rem; }
.network-bar { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; flex: 1; min-width: 280px; }
.network-bar-label { font-size: 0.85rem; color: #8b9bb4; }
.network-bar select, .network-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; }
.network-bar input { width: 10rem; }
.network-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
.network-bar-status { font-size: 0.8rem; color: #6b8cae; flex: 1 1 100%; }
.auth-token-bar { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 100%; }
.auth-token-hidden { display: none; }
.auth-token-label { font-size: 0.85rem; color: #8b9bb4; }
.auth-token-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; width: 16rem; }
.auth-token-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
.core-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.35rem; vertical-align: middle; }
.core-dot.online { background: #2ecc71; }
.core-dot.offline { background: #e74c3c; }
nav { display: flex; gap: 0.5rem; flex-wrap: wrap; }
nav button { background: #2d5a8a; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; }
nav button.active { background: #4a90d9; }
main { padding: 1.5rem; }
.ringing-hidden { display: none; }
#ringing-popup { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
#ringing-popup.ringing-hidden { display: none; }
.ringing-card { background: #1a2332; padding: 2rem; border-radius: 12px; text-align: center; min-width: 280px; }
.ringing-card button { margin: 0.5rem; padding: 0.6rem 1.2rem; border: none; border-radius: 6px; cursor: pointer; }
.ringing-accept { background: #2ecc71; color: #000; }
.ringing-reject { background: #e74c3c; color: #fff; }
.peer-picker { max-width: 480px; }
.peer-picker-core { font-size: 0.75rem; color: #6b8cae; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.04em; }
.peer-network-banner { font-size: 0.85rem; color: #8b9bb4; margin-bottom: 0.75rem; padding: 0.4rem 0.5rem; background: #1a2332; border-radius: 4px; }
.peer-network-warn { color: #e6a23c; }
.peer-picker-row { display: block; margin-bottom: 0.75rem; }
.peer-picker-row select, .peer-picker-row input { width: 100%; margin-top: 0.25rem; padding: 0.4rem; background: #1a2332; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; }
.peer-list { list-style: none; padding: 0; margin: 0.5rem 0; max-height: 280px; overflow: auto; border: 1px solid #2d3a4d; border-radius: 6px; }
.peer-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; cursor: pointer; border-bottom: 1px solid #2d3a4d; }
.peer-item:last-child { border-bottom: none; }
.peer-item:hover, .peer-item.active { background: #243044; }
.peer-dot { width: 8px; height: 8px; border-radius: 50%; background: #666; flex-shrink: 0; }
.peer-item.online .peer-dot { background: #2ecc71; }
.peer-main { flex: 1; min-width: 0; }
.peer-name { display: block; }
.peer-sub { color: #8b9bb4; font-size: 0.8rem; }
.peer-edit { background: transparent; border: none; color: #8b9bb4; cursor: pointer; padding: 0.25rem; }
.peer-empty { padding: 1rem; color: #8b9bb4; }
.peer-selected { margin: 0.5rem 0; min-height: 1.25rem; }
.chat-layout { display: flex; gap: 1rem; min-height: 400px; }
.chat-peers { flex: 0 0 280px; }
.chat-thread-panel { flex: 1; display: flex; flex-direction: column; }
.chat-thread-header { padding-bottom: 0.5rem; border-bottom: 1px solid #2d3a4d; margin-bottom: 0.5rem; }
#chat-thread { flex: 1; overflow: auto; border: 1px solid #2d3a4d; padding: 0.5rem; border-radius: 6px; margin-bottom: 0.5rem; }
.video-remote-label { position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.6); padding: 2px 6px; font-size: 0.75rem; border-radius: 4px; }
.video-wrap { position: relative; display: inline-block; }
+30
View File
@@ -0,0 +1,30 @@
{
"id": "chat",
"name": "Chat P2P",
"version": "1.17.1",
"requires": ["comms-core", "webhub"],
"backoneCore": {
"apis": [
"/app/api/peers",
"/app/api/networks",
"POST /app/api/network/{nwid}",
"DELETE /app/api/network/{nwid}",
"/app/api/network/{nwid}",
"/app/api/network/{nwid}/members",
"/app/api/authtoken",
"/app/api/contacts"
]
},
"backend": {
"routes": ["/controller/comms/network"],
"websocketEvents": ["chat_message", "typing"]
},
"frontend": {
"entry": "/app/addons/chat/index.js",
"navLabel": "Chat",
"icon": "chat"
},
"settings": {
"attachmentMaxBytes": 10485760
}
}
+197
View File
@@ -0,0 +1,197 @@
window.BackOneAddons = window.BackOneAddons || {};
window.BackOneAddons.chat = function chatAddon() {
const messagesByPeer = {};
let selectedPeer = null;
let root = null;
let unsubChat = null;
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function clearSessionMessages() {
Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
}
function getThread(peerId) {
if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
return messagesByPeer[peerId];
}
function formatTime(ts) {
if (!ts) return '';
const d = new Date(ts);
return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
}
function networkLabel() {
const n = BackOneNetworkSession.getActiveNetwork();
return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
}
function updateConnectionBar() {
const bar = root?.querySelector('#chat-conn-bar');
if (!bar || !window.BackOneCommLayout) return;
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
selectedPeer,
BackOneNetworkSession.getActiveNetwork()
);
}
function renderHeader() {
const header = root?.querySelector('#chat-header');
if (!header) return;
const net = networkLabel();
if (!selectedPeer) {
header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
return;
}
header.innerHTML = `<strong>${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)}</strong> <small>${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}</small>`;
updateConnectionBar();
}
function selfName() {
const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
return addr ? addr.slice(0, 10) + '…' : 'me';
}
return {
mount(container) {
root = container;
clearSessionMessages();
container.innerHTML = `
<div class="comm-layout comm-split-layout">
<aside class="comm-sidebar">
<div class="comm-sidebar-head">
<h2>Chat P2P</h2>
<p class="comm-sidebar-sub">Contacts · session only</p>
</div>
<div class="comm-sidebar-body">
<div id="chat-peer-picker"></div>
</div>
</aside>
<div class="comm-main">
<div class="chat-thread-panel">
<div class="chat-thread-header" id="chat-header">Select a contact</div>
<div id="chat-thread" class="chat-messages"></div>
<div class="chat-compose">
<textarea id="chat-input" rows="2" placeholder="Type a message" disabled></textarea>
<div class="chat-compose-row">
<button type="button" id="chat-send" class="btn-primary" disabled>Send</button>
</div>
</div>
</div>
</div>
</div>
<div id="chat-conn-bar" class="comm-connection-bar"></div>`;
const thread = container.querySelector('#chat-thread');
const input = container.querySelector('#chat-input');
const sendBtn = container.querySelector('#chat-send');
const render = () => {
if (!selectedPeer) {
thread.innerHTML = '<p class="chat-empty">Choose a contact from the list</p>';
return;
}
const messages = getThread(selectedPeer.peerId);
if (!messages.length) {
thread.innerHTML = '<p class="chat-empty">No messages yet — start the conversation</p>';
return;
}
thread.innerHTML = messages.map(m => `
<div class="chat-message">
<div class="chat-message-meta">
<span class="chat-message-sender">${escapeHtml(m.senderName || m.sender || 'peer')}</span>
<span class="chat-message-time">${escapeHtml(formatTime(m.at))}</span>
</div>
${m.body ? `<div class="chat-message-body">${escapeHtml(m.body)}</div>` : ''}
</div>`).join('');
thread.scrollTop = thread.scrollHeight;
};
function updateInputs() {
const networkId = BackOneNetworkSession.getNetworkId();
const ok = !!networkId && !!selectedPeer;
input.disabled = !ok;
sendBtn.disabled = !ok;
renderHeader();
}
if (unsubChat) unsubChat();
unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
const networkId = BackOneNetworkSession.getNetworkId();
if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
const senderId = (msg.senderId || '').toLowerCase();
const peerId = selectedPeer?.peerId?.toLowerCase();
if (!peerId || senderId !== peerId) {
if (senderId) {
getThread(senderId).push({
sender: senderId,
senderName: msg.senderName || senderId,
body: msg.body || '',
at: msg.at || Date.now()
});
}
return;
}
getThread(peerId).push({
sender: senderId,
senderName: msg.senderName || senderId,
body: msg.body || '',
at: msg.at || Date.now()
});
render();
});
BackOnePeerPicker(container.querySelector('#chat-peer-picker'), {
layout: 'sidebar',
showActionButton: false,
onSelect(peer) {
selectedPeer = peer;
updateInputs();
render();
},
onNetworkChange() {
selectedPeer = null;
clearSessionMessages();
updateInputs();
render();
}
});
BackOneNetworkSession.subscribe(() => {
updateInputs();
render();
});
sendBtn.onclick = () => {
const networkId = BackOneNetworkSession.getNetworkId();
if (!selectedPeer || !networkId) return;
const body = input.value.trim();
if (!body) return;
const now = Date.now();
const name = selfName();
BackOneWebHubClient.send({
type: 'chat_message',
networkId,
targetId: selectedPeer.peerId,
senderName: name,
body
});
getThread(selectedPeer.peerId).push({
sender: 'me',
senderName: name,
body,
at: now
});
input.value = '';
render();
};
updateInputs();
render();
}
};
};
+10
View File
@@ -0,0 +1,10 @@
{
"id": "comms-core",
"name": "Comms Core",
"version": "1.17.1",
"requires": [],
"backend": {
"routes": ["/controller/comms"],
"websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
}
}
+32
View File
@@ -0,0 +1,32 @@
-- BackOne comms-core addon schema (PostgreSQL)
CREATE TABLE IF NOT EXISTS backone_calls (
id TEXT PRIMARY KEY,
network_id TEXT NOT NULL,
caller_id TEXT NOT NULL,
callee_id TEXT NOT NULL,
call_type TEXT NOT NULL CHECK (call_type IN ('voice', 'video')),
state TEXT NOT NULL,
sdp_offer TEXT,
sdp_answer TEXT,
created_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS backone_chat_messages (
id TEXT PRIMARY KEY,
network_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
body TEXT,
attachment_id TEXT,
created_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS backone_attachments (
id TEXT PRIMARY KEY,
network_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
storage_path TEXT NOT NULL,
created_at BIGINT NOT NULL
);
+27
View File
@@ -0,0 +1,27 @@
{
"id": "secure-chat",
"name": "Secure Chat",
"version": "1.17.1",
"requires": ["comms-core", "webhub"],
"backoneCore": {
"apis": [
"/app/api/peers",
"/app/api/networks",
"POST /app/api/network/{nwid}",
"DELETE /app/api/network/{nwid}",
"/app/api/network/{nwid}",
"/app/api/network/{nwid}/members",
"/app/api/authtoken",
"/app/api/contacts"
]
},
"backend": {
"routes": ["/controller/comms/network"],
"websocketEvents": ["secure_chat_message"]
},
"frontend": {
"entry": "/app/addons/secure-chat/index.js",
"navLabel": "Secure Chat",
"icon": "chat"
}
}
+186
View File
@@ -0,0 +1,186 @@
window.BackOneAddons = window.BackOneAddons || {};
window.BackOneAddons['secure-chat'] = function secureChatAddon() {
const messagesByPeer = {};
let selectedPeer = null;
let root = null;
let unsubChat = null;
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function clearSessionMessages() {
Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
}
function getThread(peerId) {
if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
return messagesByPeer[peerId];
}
function formatTime(ts) {
if (!ts) return '';
const d = new Date(ts);
return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
}
function networkLabel() {
const n = BackOneNetworkSession.getActiveNetwork();
return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
}
function renderHeader() {
const header = root?.querySelector('#secure-chat-header');
if (!header) return;
const net = networkLabel();
if (!selectedPeer) {
header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
return;
}
header.innerHTML = `<strong>${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)}</strong> <small>${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}</small>`;
}
function selfName() {
const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
return addr ? addr.slice(0, 10) + '…' : 'me';
}
return {
mount(container) {
root = container;
clearSessionMessages();
container.innerHTML = `
<div class="comm-layout comm-split-layout">
<aside class="comm-sidebar">
<div class="comm-sidebar-head">
<h2>Secure Chat</h2>
<p class="comm-sidebar-sub">Contacts · session only</p>
</div>
<div class="comm-sidebar-body">
<div id="secure-chat-peer-picker"></div>
</div>
</aside>
<div class="comm-main">
<div class="chat-thread-panel">
<div class="chat-thread-header" id="secure-chat-header">Select a contact</div>
<div id="secure-chat-thread" class="chat-messages"></div>
<div class="chat-compose">
<textarea id="secure-chat-input" rows="2" placeholder="Type a secure message" disabled></textarea>
<div class="chat-compose-row">
<button type="button" id="secure-chat-send" class="btn-primary" disabled>Send</button>
</div>
</div>
</div>
</div>
</div>`;
const thread = container.querySelector('#secure-chat-thread');
const input = container.querySelector('#secure-chat-input');
const sendBtn = container.querySelector('#secure-chat-send');
const render = () => {
if (!selectedPeer) {
thread.innerHTML = '<p class="chat-empty">Choose a contact from the list</p>';
return;
}
const messages = getThread(selectedPeer.peerId);
if (!messages.length) {
thread.innerHTML = '<p class="chat-empty">No messages yet — start the conversation</p>';
return;
}
thread.innerHTML = messages.map(m => `
<div class="chat-message">
<div class="chat-message-meta">
<span class="chat-message-sender">${escapeHtml(m.senderName || m.sender || 'me')}</span>
<span class="chat-message-time">${escapeHtml(formatTime(m.at))}</span>
</div>
${m.body ? `<div class="chat-message-body">${escapeHtml(m.body)}</div>` : ''}
</div>`).join('');
thread.scrollTop = thread.scrollHeight;
};
function updateInputs() {
const networkId = BackOneNetworkSession.getNetworkId();
const ok = !!networkId && !!selectedPeer;
input.disabled = !ok;
sendBtn.disabled = !ok;
renderHeader();
}
if (unsubChat) unsubChat();
unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
const networkId = BackOneNetworkSession.getNetworkId();
if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
const senderId = (msg.senderId || '').toLowerCase();
const peerId = selectedPeer?.peerId?.toLowerCase();
if (!peerId || senderId !== peerId) {
if (senderId) {
getThread(senderId).push({
sender: senderId,
senderName: msg.senderName || senderId,
body: msg.body || '',
at: msg.at || Date.now()
});
}
return;
}
getThread(peerId).push({
sender: senderId,
senderName: msg.senderName || senderId,
body: msg.body || '',
at: msg.at || Date.now()
});
render();
});
BackOnePeerPicker(container.querySelector('#secure-chat-peer-picker'), {
layout: 'sidebar',
showActionButton: false,
onSelect(peer) {
selectedPeer = peer;
updateInputs();
render();
},
onNetworkChange() {
selectedPeer = null;
clearSessionMessages();
updateInputs();
render();
}
});
BackOneNetworkSession.subscribe(() => {
updateInputs();
render();
});
sendBtn.onclick = () => {
const networkId = BackOneNetworkSession.getNetworkId();
if (!selectedPeer || !networkId) return;
const body = input.value.trim();
if (!body) return;
const now = Date.now();
const name = selfName();
BackOneWebHubClient.send({
type: 'chat_message',
networkId,
targetId: selectedPeer.peerId,
senderName: name,
body
});
getThread(selectedPeer.peerId).push({
sender: 'me',
senderName: name,
body,
at: now
});
input.value = '';
render();
};
updateInputs();
render();
}
};
};
+10
View File
@@ -0,0 +1,10 @@
{
"id": "updater",
"name": "BackOne Updater",
"version": "1.17.1",
"requires": [],
"backend": {
"routes": [],
"websocketEvents": []
}
}
+132
View File
@@ -0,0 +1,132 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
# shellcheck source=platform-paths.sh
. "${SCRIPT_DIR}/platform-paths.sh"
else
# shellcheck source=../../pkg/common/platform-paths.sh
. "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
fi
backone_resolve_paths
HOME_DIR="${BACKONE_HOME}"
STAMP="${HOME_DIR}/.installed-version"
LOG_DIR="${BACKONE_LOG_DIR:-/var/log/backone}"
LOG_FILE="${LOG_DIR}/updater.log"
MIGRATIONS="${BACKONE_ADDONS_DIR}/updater/migrations"
LEGACY_HOME="/var/lib/zerotier-one"
log() {
mkdir -p "$LOG_DIR"
echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
}
read_version() {
if [ -f "$STAMP" ]; then
cat "$STAMP"
else
echo '{"package":"backone","version":"0.0.0","schemaVersion":0}'
fi
}
write_version() {
local version="${1:-$(cat debian/version 2>/dev/null || echo 1.15.0)}"
mkdir -p "$HOME_DIR"
cat > "$STAMP" <<EOF
{"package":"backone","version":"${version}","schemaVersion":2,"updatedAt":"$(date -Iseconds)"}
EOF
chown backone:backone "$STAMP" 2>/dev/null || true
}
run_migrations() {
local scope="${1:-all}"
[ -d "$MIGRATIONS" ] || return 0
for script in "$MIGRATIONS"/*; do
[ -f "$script" ] || continue
case "$(basename "$script")" in
001_*)
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
;;
002_*)
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
;;
003_*)
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
;;
004_*|005_*)
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
;;
*)
if [ "$scope" != "all" ] && [ "$scope" != "$ADDON_SCOPE" ]; then
continue
fi
;;
esac
log "Running migration: $(basename "$script")"
BACKONE_HOME="$HOME_DIR" bash "$script" || log "Migration warning: $(basename "$script")"
done
}
migrate_zerotier() {
if [ -d "$LEGACY_HOME" ] && [ ! -f "$HOME_DIR/identity.public" ]; then
log "Migrating $LEGACY_HOME -> $HOME_DIR"
mkdir -p "$HOME_DIR"
cp -a "$LEGACY_HOME/." "$HOME_DIR/" 2>/dev/null || true
ln -sfn "$HOME_DIR" "$LEGACY_HOME" 2>/dev/null || true
fi
}
rebuild_manifest() {
local registry="${HOME_DIR}/addons.d/enabled.json"
mkdir -p "${HOME_DIR}/addons.d"
if [ ! -f "$registry" ]; then
echo '[]' > "$registry"
fi
local script_dir manifest_script
script_dir="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
manifest_script="${script_dir}/rebuild-addon-manifest.sh"
if [ ! -x "$manifest_script" ]; then
echo "backone-updater: ${manifest_script} not found" >&2
exit 1
fi
local count
count="$(BACKONE_HOME="$HOME_DIR" BACKONE_ADDONS_DIR="${BACKONE_ADDONS_DIR}" bash "$manifest_script")"
log "Wrote ${HOME_DIR}/app/addons/manifest.json (${count} UI addons)"
}
cmd_migrate() {
local scope="all"
ADDON_SCOPE=""
while [ $# -gt 0 ]; do
case "$1" in
--core) scope="core"; shift ;;
--addon) ADDON_SCOPE="$2"; scope="addon"; shift 2 ;;
--dry-run)
log "Dry run: would migrate scope=$scope addon=$ADDON_SCOPE"
ls -1 "$MIGRATIONS" 2>/dev/null || true
exit 0
;;
*) shift ;;
esac
done
migrate_zerotier
run_migrations "$scope"
write_version
rebuild_manifest
log "Migration complete"
}
cmd_status() {
echo "Installed: $(read_version)"
echo "Package version: $(cat "${BACKONE_ADDONS_DIR}/comms-core/addon.json" 2>/dev/null | grep '"version"' | head -1 || cat debian/version 2>/dev/null || echo unknown)"
echo "Addons registry: ${HOME_DIR}/addons.d/enabled.json"
}
case "${1:-}" in
migrate) shift; cmd_migrate "$@" ;;
status) cmd_status ;;
rebuild-manifest) rebuild_manifest ;;
*) echo "Usage: backone-updater migrate [--core|--addon ID] [--dry-run] | status | rebuild-manifest" >&2; exit 1 ;;
esac
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# Deep-merge JSON into /var/lib/backone/local.conf (idempotent).
set -euo pipefail
HOME_DIR="${BACKONE_HOME:-/var/lib/backone}"
CONF="${HOME_DIR}/local.conf"
PATCH="${1:-}"
if [ -z "$PATCH" ] && [ ! -t 0 ]; then
PATCH="$(cat)"
fi
if [ -z "$PATCH" ]; then
echo "Usage: merge-local-conf.sh '<json>' or pipe JSON on stdin" >&2
exit 1
fi
python3 - "$CONF" "$PATCH" <<'PY'
import json
import os
import sys
conf_path, patch_raw = sys.argv[1], sys.argv[2]
def deep_merge(base, patch):
if not isinstance(base, dict) or not isinstance(patch, dict):
return patch
out = dict(base)
for k, v in patch.items():
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
out[k] = deep_merge(out[k], v)
else:
out[k] = v
return out
patch = json.loads(patch_raw)
current = {}
if os.path.isfile(conf_path):
with open(conf_path, encoding="utf-8") as f:
try:
current = json.load(f)
except json.JSONDecodeError:
current = {}
if not isinstance(current, dict):
current = {}
merged = deep_merge(current, patch)
os.makedirs(os.path.dirname(conf_path), exist_ok=True)
with open(conf_path, "w", encoding="utf-8") as f:
json.dump(merged, f, indent=2)
f.write("\n")
PY
chown backone:backone "$CONF" 2>/dev/null || true
chmod 644 "$CONF" 2>/dev/null || true
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
LEGACY="/var/lib/zerotier-one"
if [ -d "$LEGACY" ] && [ ! -f "$BACKONE_HOME/identity.public" ]; then
mkdir -p "$BACKONE_HOME"
cp -an "$LEGACY/." "$BACKONE_HOME/" 2>/dev/null || cp -a "$LEGACY/." "$BACKONE_HOME/"
fi
if ! getent passwd backone >/dev/null 2>&1 && getent passwd zerotier-one >/dev/null 2>&1; then
usermod -l backone -d "$BACKONE_HOME" zerotier-one 2>/dev/null || true
fi
chown -R backone:backone "$BACKONE_HOME" 2>/dev/null || true
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
mkdir -p "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app/addons"
if [ ! -f "$BACKONE_HOME/addons.d/enabled.json" ]; then
echo '[]' > "$BACKONE_HOME/addons.d/enabled.json"
fi
chown -R backone:backone "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app" 2>/dev/null || true
@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{"settings":{"enableWebServer":true}}'
fi
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{
"settings": {
"icePreferOverlay": true,
"iceServers": [{"urls": "stun:stun.l.google.com:19302"}],
"iceFallbackTimeoutMs": 8000,
"webhubPort": 9994
}
}'
fi
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
set -euo pipefail
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
CONF="${BACKONE_HOME}/local.conf"
python3 - "$CONF" <<'PY'
import json
import os
import sys
path = sys.argv[1]
defaults = ["comms-core", "webhub", "voice", "video", "chat"]
current = {}
if os.path.isfile(path):
with open(path, encoding="utf-8") as f:
try:
current = json.load(f)
except json.JSONDecodeError:
current = {}
if not isinstance(current, dict):
current = {}
settings = current.setdefault("settings", {})
if not isinstance(settings, dict):
settings = {}
current["settings"] = settings
enabled = settings.get("enabledAddons")
if enabled is None:
# No explicit list — all installed addons stay enabled.
sys.exit(0)
if not isinstance(enabled, list):
enabled = []
merged = list(enabled)
for addon in defaults:
if addon not in merged:
merged.append(addon)
settings["enabledAddons"] = merged
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(current, f, indent=2)
f.write("\n")
PY
chown backone:backone "$CONF" 2>/dev/null || true
chmod 644 "$CONF" 2>/dev/null || true
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
# Writes $BACKONE_HOME/app/addons/manifest.json from installed addon.json files.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
# shellcheck source=platform-paths.sh
. "${SCRIPT_DIR}/platform-paths.sh"
else
# shellcheck source=../../pkg/common/platform-paths.sh
. "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
fi
backone_resolve_paths
HOME_DIR="${BACKONE_HOME}"
ADDONS_DIR="${BACKONE_ADDONS_DIR}"
MANIFEST="${HOME_DIR}/app/addons/manifest.json"
mkdir -p "${HOME_DIR}/app/addons" "${HOME_DIR}/addons.d"
if ! command -v python3 >/dev/null 2>&1; then
echo "rebuild-addon-manifest: python3 required" >&2
exit 1
fi
python3 - "$ADDONS_DIR" "$MANIFEST" <<'PY'
import json
import os
import sys
addons_dir, manifest_path = sys.argv[1], sys.argv[2]
out = []
if os.path.isdir(addons_dir):
for name in sorted(os.listdir(addons_dir)):
path = os.path.join(addons_dir, name, "addon.json")
if not os.path.isfile(path):
continue
with open(path, encoding="utf-8") as f:
m = json.load(f)
if "frontend" not in m:
continue
item = {
"id": m["id"],
"name": m.get("name", m["id"]),
"version": m.get("version", "0.0.0"),
"frontend": m["frontend"],
}
if "requires" in m:
item["requires"] = m["requires"]
out.append(item)
os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
with open(manifest_path, "w", encoding="utf-8") as f:
json.dump(out, f, indent=2)
f.write("\n")
print(len(out))
PY
chown backone:backone "$MANIFEST" 2>/dev/null || true
chmod 644 "$MANIFEST" 2>/dev/null || true
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
set -e
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
SECRET="${BACKONE_HOME}/authtoken.secret"
trim_token() {
tr -d '\n\r \t' < "$1"
}
sync_for_user() {
user="$1"
home="$2"
[ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
user_token="${home}/.backOneOneAuthToken"
if [ -f "$user_token" ] && [ -s "$user_token" ]; then
ut="$(trim_token "$user_token")"
[ -n "$ut" ] || return 0
st=""
if [ -f "$SECRET" ]; then
st="$(trim_token "$SECRET" 2>/dev/null || true)"
fi
if [ "$ut" != "$st" ]; then
printf '%s' "$ut" > "$SECRET"
chown backone:backone "$SECRET" 2>/dev/null || true
chmod 0600 "$SECRET" 2>/dev/null || true
echo "Synced ${SECRET} from ${user_token}"
fi
return 0
fi
if [ -f "$SECRET" ]; then
install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
|| cp "$SECRET" "$user_token"
chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
chmod 0600 "$user_token" 2>/dev/null || true
echo "Installed ${user_token} from ${SECRET}"
fi
}
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
sync_for_user "$SUDO_USER" "$home"
fi
if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
home="$(getent passwd "$USER" | cut -d: -f6)"
sync_for_user "$USER" "$home"
fi
exit 0
+30
View File
@@ -0,0 +1,30 @@
{
"id": "video",
"name": "Video Call P2P",
"version": "1.17.1",
"requires": ["comms-core", "webhub", "voice"],
"backoneCore": {
"apis": [
"/app/api/peers",
"/app/api/networks",
"POST /app/api/network/{nwid}",
"DELETE /app/api/network/{nwid}",
"/app/api/network/{nwid}",
"/app/api/network/{nwid}/members",
"/app/api/authtoken",
"/app/api/contacts"
]
},
"backend": {
"routes": ["/controller/comms/call"],
"websocketEvents": ["incoming_call", "call_state"]
},
"frontend": {
"entry": "/app/addons/video/index.js",
"navLabel": "Video",
"icon": "video"
},
"settings": {
"enableVideoCall": true
}
}
+226
View File
@@ -0,0 +1,226 @@
window.BackOneAddons = window.BackOneAddons || {};
window.BackOneAddons.video = function videoAddon() {
let picker = null;
let selectedPeer = null;
let inCall = false;
let micEnabled = true;
let camEnabled = true;
let root = null;
function updateConnectionBar() {
const bar = root?.querySelector('#video-conn-bar');
if (!bar || !window.BackOneCommLayout) return;
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
selectedPeer,
BackOneNetworkSession.getActiveNetwork()
);
}
function updateHeadStatus() {
const statusEl = root?.querySelector('#video-head-status');
const breadcrumb = root?.querySelector('#video-breadcrumb-peer');
if (!statusEl) return;
if (inCall && selectedPeer) {
statusEl.innerHTML = `<span class="connected">Connected</span> · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
} else if (selectedPeer) {
statusEl.textContent = 'Ready';
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
} else {
statusEl.textContent = '';
if (breadcrumb) breadcrumb.textContent = '—';
}
}
function updateCallButtons() {
const callBtn = root?.querySelector('#video-call');
if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
const selectedLabel = root?.querySelector('#video-selected-label');
if (selectedLabel) {
selectedLabel.innerHTML = selectedPeer
? `Selected: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
: 'Selected: <strong>—</strong>';
}
updateHeadStatus();
updateConnectionBar();
}
function updateCallLayout() {
const layout = root?.querySelector('.comm-layout');
if (layout) layout.classList.toggle('video-in-call', inCall);
}
function showPlaceholder(show) {
const stage = root?.querySelector('.video-stage');
const remote = root?.querySelector('#video-remote');
const phLabel = root?.querySelector('#video-ph-label');
if (stage) stage.classList.toggle('has-remote', !show);
if (remote) remote.classList.toggle('active', !show);
if (phLabel && show) {
phLabel.textContent = inCall
? 'Waiting for remote peer…'
: (selectedPeer ? 'Ready to call' : 'Select a peer to start a video call');
}
updateCallLayout();
}
function bindStreams() {
const local = root?.querySelector('#video-local');
const remote = root?.querySelector('#video-remote');
const localStream = BackOneWebRtcSession.getLocalStream();
const remoteStream = BackOneWebRtcSession.getRemoteStream();
if (local && localStream) local.srcObject = localStream;
if (remote && remoteStream && remoteStream.getTracks().length) {
remote.srcObject = remoteStream;
showPlaceholder(false);
root?.querySelector('#video-breadcrumb-state')?.textContent = 'Connected to';
}
}
async function hangUp() {
await BackOneWebRtcSession.hangUp();
const local = root?.querySelector('#video-local');
const remote = root?.querySelector('#video-remote');
if (local) local.srcObject = null;
if (remote) remote.srcObject = null;
inCall = false;
micEnabled = true;
camEnabled = true;
const breadcrumbState = root?.querySelector('#video-breadcrumb-state');
if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
showPlaceholder(true);
updateCallButtons();
window.BackOneRingingPopup?.dismiss();
}
return {
mount(container) {
root = container;
container.innerHTML = `
<div class="comm-layout">
<aside class="comm-sidebar">
<div class="comm-sidebar-head">
<h2>Video Call P2P</h2>
<p class="comm-sidebar-sub">BackOne core · same network as backone-cli</p>
</div>
<div class="comm-sidebar-body">
<div id="video-picker"></div>
</div>
<div class="comm-sidebar-actions">
<div id="video-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
<button type="button" id="video-call" class="btn-primary" disabled>Video Call</button>
<button type="button" id="video-hangup" class="btn-secondary">Hang Up</button>
</div>
</aside>
<div class="comm-main">
<div class="comm-main-head">
<div class="comm-breadcrumb">Video Call P2P &gt; <span id="video-breadcrumb-state">Select peer</span> <strong id="video-breadcrumb-peer">—</strong></div>
<div id="video-head-status" class="comm-head-status"></div>
</div>
<div class="comm-stage video-stage">
<div class="video-remote-main">
<div id="video-placeholder" class="comm-remote-placeholder">
<div class="avatar">👤</div>
<div class="peer-id" id="video-ph-peer">—</div>
<div id="video-ph-label">Select a peer to start a video call</div>
</div>
<video id="video-remote" class="video-remote-stream" autoplay playsinline></video>
</div>
<div class="video-local-pip">
<span class="video-pip-label">You</span>
<video id="video-local" autoplay muted playsinline></video>
</div>
<div class="comm-remote-tag"><span class="dot"></span> Remote</div>
<div class="comm-call-controls">
<button type="button" id="video-mic">Mic</button>
<button type="button" id="video-cam">Camera</button>
<button type="button" id="video-fs">Fullscreen</button>
<button type="button" id="video-hangup-stage" class="danger">Hang Up</button>
</div>
</div>
</div>
</div>
<div id="video-conn-bar" class="comm-connection-bar"></div>`;
const callBtn = container.querySelector('#video-call');
picker = BackOnePeerPicker(container.querySelector('#video-picker'), {
layout: 'sidebar',
showActionButton: false,
onSelect(peer) {
selectedPeer = peer;
const phPeer = root.querySelector('#video-ph-peer');
if (phPeer) phPeer.textContent = peer.peerId;
if (!inCall) showPlaceholder(true);
updateCallButtons();
},
onNetworkChange() {
if (!inCall) selectedPeer = null;
updateCallButtons();
}
});
BackOneNetworkSession.subscribe(() => updateCallButtons());
BackOneWebRtcSession.subscribe((event) => {
if (event === 'call-started') {
inCall = true;
updateCallButtons();
bindStreams();
}
if (event === 'remote-track' || event === 'call-accepted') bindStreams();
if (event === 'call-ended') {
inCall = false;
showPlaceholder(true);
updateCallButtons();
}
});
callBtn.onclick = async () => {
const peer = selectedPeer || picker.getSelectedPeer();
if (!peer || !BackOneNetworkSession.getNetworkId()) return;
try {
inCall = true;
root.querySelector('#video-breadcrumb-state').textContent = 'Connecting to';
updateCallLayout();
updateCallButtons();
await BackOneWebRtcSession.startOutgoingCall(peer, 'video');
bindStreams();
} catch (err) {
await hangUp();
alert(err.message || 'Call failed');
}
};
container.querySelector('#video-hangup').onclick = () => hangUp();
container.querySelector('#video-hangup-stage').onclick = () => hangUp();
container.querySelector('#video-mic').onclick = () => {
const localStream = BackOneWebRtcSession.getLocalStream();
if (!localStream) return;
micEnabled = !micEnabled;
localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
container.querySelector('#video-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
};
container.querySelector('#video-cam').onclick = () => {
const localStream = BackOneWebRtcSession.getLocalStream();
if (!localStream) return;
camEnabled = !camEnabled;
localStream.getVideoTracks().forEach(t => { t.enabled = camEnabled; });
container.querySelector('#video-cam').textContent = camEnabled ? 'Camera' : 'Camera off';
};
container.querySelector('#video-fs').onclick = () => {
const stage = container.querySelector('.comm-stage');
if (stage.requestFullscreen) stage.requestFullscreen();
};
const active = BackOneWebRtcSession.getActiveCall();
inCall = !!active && active.callType === 'video';
showPlaceholder(true);
if (inCall) bindStreams();
updateCallButtons();
}
};
};
+31
View File
@@ -0,0 +1,31 @@
{
"id": "voice",
"name": "Voice Call P2P",
"version": "1.17.1",
"requires": ["comms-core", "webhub"],
"backoneCore": {
"apis": [
"/app/api/peers",
"/app/api/networks",
"POST /app/api/network/{nwid}",
"DELETE /app/api/network/{nwid}",
"/app/api/network/{nwid}",
"/app/api/network/{nwid}/members",
"/app/api/authtoken",
"/app/api/contacts"
]
},
"backend": {
"routes": ["/controller/comms/call"],
"websocketEvents": ["incoming_call", "call_state"]
},
"frontend": {
"entry": "/app/addons/voice/index.js",
"navLabel": "Voice",
"icon": "phone"
},
"settings": {
"enableVoiceCall": true,
"ringTimeoutSec": 30
}
}
+181
View File
@@ -0,0 +1,181 @@
window.BackOneAddons = window.BackOneAddons || {};
window.BackOneAddons.voice = function voiceAddon() {
let picker = null;
let selectedPeer = null;
let inCall = false;
let micEnabled = true;
let root = null;
let remoteAudio = null;
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function updateConnectionBar() {
const bar = root?.querySelector('#voice-conn-bar');
if (!bar || !window.BackOneCommLayout) return;
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
selectedPeer,
BackOneNetworkSession.getActiveNetwork()
);
}
function updateHeadStatus() {
const statusEl = root?.querySelector('#voice-head-status');
const breadcrumb = root?.querySelector('#voice-breadcrumb-peer');
if (!statusEl) return;
if (inCall && selectedPeer) {
statusEl.innerHTML = `<span class="connected">Connected</span> · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
} else if (selectedPeer) {
statusEl.textContent = 'Ready';
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
} else {
statusEl.textContent = '';
if (breadcrumb) breadcrumb.textContent = '—';
}
}
function updateActiveBanner() {
const banner = root?.querySelector('#voice-active-banner');
const label = root?.querySelector('#voice-active-label');
if (!banner) return;
banner.style.display = inCall && selectedPeer ? 'flex' : 'none';
if (label && selectedPeer) {
label.textContent = `In call with ${selectedPeer.displayName || selectedPeer.peerId}`;
}
}
function updateCallButtons() {
const callBtn = root?.querySelector('#voice-call');
if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
const selectedLabel = root?.querySelector('#voice-selected-label');
if (selectedLabel) {
selectedLabel.innerHTML = selectedPeer
? `Selected: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
: 'Selected: <strong>—</strong>';
}
updateHeadStatus();
updateConnectionBar();
updateActiveBanner();
}
function syncInCallState() {
const active = BackOneWebRtcSession.getActiveCall();
inCall = !!active && (active.callType === 'voice' || !active.callType);
if (inCall && active.peer) {
selectedPeer = active.peer.peerId ? active.peer : selectedPeer;
root?.querySelector('#voice-breadcrumb-state')?.textContent = 'Connected to';
}
updateCallButtons();
}
async function hangUp() {
await BackOneWebRtcSession.hangUp();
inCall = false;
micEnabled = true;
if (remoteAudio) remoteAudio.srcObject = null;
const breadcrumbState = root?.querySelector('#voice-breadcrumb-state');
if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
updateCallButtons();
window.BackOneRingingPopup?.dismiss();
}
return {
mount(container) {
root = container;
container.innerHTML = `
<div class="comm-layout comm-split-layout">
<aside class="comm-sidebar">
<div class="comm-sidebar-head">
<h2>Voice Call P2P</h2>
<p class="comm-sidebar-sub">Contacts</p>
</div>
<div class="comm-sidebar-body">
<div id="voice-picker"></div>
</div>
<div class="comm-sidebar-actions">
<div id="voice-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
<button type="button" id="voice-call" class="btn-primary" disabled>Voice Call</button>
<button type="button" id="voice-hangup" class="btn-secondary">Hang Up</button>
</div>
</aside>
<div class="comm-main">
<div class="comm-main-head">
<div class="comm-breadcrumb">Voice Call P2P &gt; <span id="voice-breadcrumb-state">Select peer</span> <strong id="voice-breadcrumb-peer">—</strong></div>
<div id="voice-head-status" class="comm-head-status"></div>
</div>
<div class="voice-main-panel">
<div id="voice-active-banner" class="voice-active-banner" style="display:none">
<div>
<span class="voice-active-dot"></span>
<span id="voice-active-label">In call</span>
</div>
<div class="voice-active-controls">
<button type="button" id="voice-mic">Mic</button>
<button type="button" id="voice-hangup-stage" class="danger">Hang Up</button>
</div>
</div>
<p class="voice-session-hint">Live calls only — no call history is stored.</p>
<audio id="voice-remote-audio" autoplay playsinline class="voice-remote-audio"></audio>
</div>
</div>
</div>
<div id="voice-conn-bar" class="comm-connection-bar"></div>`;
remoteAudio = container.querySelector('#voice-remote-audio');
const callBtn = container.querySelector('#voice-call');
picker = BackOnePeerPicker(container.querySelector('#voice-picker'), {
layout: 'sidebar',
showActionButton: false,
onSelect(peer) {
selectedPeer = peer;
updateCallButtons();
},
onNetworkChange() {
if (!inCall) selectedPeer = null;
updateCallButtons();
}
});
BackOneNetworkSession.subscribe(() => updateCallButtons());
BackOneWebRtcSession.subscribe((event) => {
if (event === 'call-started' || event === 'call-ended' || event === 'remote-track') {
syncInCallState();
const stream = BackOneWebRtcSession.getRemoteStream();
if (remoteAudio && stream) remoteAudio.srcObject = stream;
}
});
callBtn.onclick = async () => {
const peer = selectedPeer || picker.getSelectedPeer();
if (!peer || !BackOneNetworkSession.getNetworkId()) return;
try {
await BackOneWebRtcSession.startOutgoingCall(peer, 'voice');
inCall = true;
root.querySelector('#voice-breadcrumb-state').textContent = 'Connecting to';
updateCallButtons();
} catch (err) {
await hangUp();
alert(err.message || 'Call failed');
}
};
container.querySelector('#voice-hangup').onclick = () => hangUp();
container.querySelector('#voice-hangup-stage').onclick = () => hangUp();
container.querySelector('#voice-mic').onclick = () => {
const localStream = BackOneWebRtcSession.getLocalStream();
if (!localStream) return;
micEnabled = !micEnabled;
localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
container.querySelector('#voice-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
};
syncInCallState();
updateCallButtons();
}
};
};
+15
View File
@@ -0,0 +1,15 @@
{
"id": "webhub",
"name": "WebSocket Signaling Hub",
"version": "1.17.1",
"requires": ["comms-core"],
"backend": {
"routes": [],
"websocketEvents": ["presence", "incoming_call", "call_state", "ice_candidate", "chat_message", "typing"],
"sidecar": {
"command": "/usr/lib/backone/addons/webhub/backone-webhub",
"port": 9994,
"bind": "127.0.0.1"
}
}
}
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""BackOne WebHub — ephemeral in-memory WebSocket signaling (no DB, no history)."""
import asyncio
import json
import os
import secrets
import time
import urllib.parse
try:
import websockets
from websockets.server import serve
except ImportError:
raise SystemExit("python3-websockets is required")
BACKONE_HOME = os.environ.get("BACKONE_HOME", "/var/lib/backone")
AUTH_PATH = os.path.join(BACKONE_HOME, "authtoken.secret")
CONF_PATH = os.path.join(BACKONE_HOME, "local.conf")
DEFAULT_PORT = 9994
RING_TIMEOUT_SEC = 30
peers = {} # peer_id -> {ws, network_id, registered_at}
calls = {} # call_id -> call dict
ice_queues = {} # call_id -> list of {from, candidate, ...}
peer_to_calls = {} # peer_id -> set(call_id)
def load_auth_token():
try:
with open(AUTH_PATH, encoding="utf-8") as f:
t = f.read().strip()
return t if t else None
except OSError:
return None
def load_port():
try:
with open(CONF_PATH, encoding="utf-8") as f:
cfg = json.load(f)
settings = cfg.get("settings") or {}
return int(settings.get("webhubPort", DEFAULT_PORT))
except (OSError, ValueError, TypeError, json.JSONDecodeError):
return DEFAULT_PORT
def valid_token(token):
expected = load_auth_token()
if not expected or not token:
return False
return secrets.compare_digest(token.strip(), expected.strip())
def new_call_id():
return f"call_{int(time.time() * 1000)}_{secrets.token_hex(4)}"
def peer_key(peer_id):
return (peer_id or "").lower()
def send_json(ws, payload):
return ws.send(json.dumps(payload))
async def push_to_peer(target_id, payload):
key = peer_key(target_id)
entry = peers.get(key)
if not entry:
return False
try:
await send_json(entry["ws"], payload)
return True
except Exception:
return False
def track_call_for_peer(pid, call_id):
key = peer_key(pid)
if key not in peer_to_calls:
peer_to_calls[key] = set()
peer_to_calls[key].add(call_id)
def untrack_call(call_id, call):
for pid in (call.get("callerId"), call.get("calleeId")):
key = peer_key(pid)
if key in peer_to_calls:
peer_to_calls[key].discard(call_id)
if not peer_to_calls[key]:
del peer_to_calls[key]
def purge_call(call_id):
call = calls.pop(call_id, None)
ice_queues.pop(call_id, None)
if call:
untrack_call(call_id, call)
def end_calls_for_peer(peer_id):
key = peer_key(peer_id)
for call_id in list(peer_to_calls.get(key, set())):
call = calls.get(call_id)
if not call:
continue
other = call["calleeId"] if peer_key(call["callerId"]) == key else call["callerId"]
asyncio.create_task(push_to_peer(other, {
"type": "call_state",
"callId": call_id,
"state": "ended",
"reason": "peer_disconnect"
}))
purge_call(call_id)
async def ring_timeout_watch():
while True:
await asyncio.sleep(5)
now = time.time()
for call_id, call in list(calls.items()):
if call.get("state") != "ringing":
continue
if now - call.get("createdAt", now) > RING_TIMEOUT_SEC:
await push_to_peer(call["callerId"], {
"type": "call_state",
"callId": call_id,
"state": "rejected",
"reason": "timeout"
})
purge_call(call_id)
async def handle_message(ws, state, msg):
mtype = msg.get("type")
if mtype == "auth":
token = msg.get("token", "")
if not valid_token(token):
await send_json(ws, {"type": "error", "error": "unauthorized"})
await ws.close()
return False
state["authed"] = True
await send_json(ws, {"type": "auth_ok"})
return True
if not state.get("authed"):
await send_json(ws, {"type": "error", "error": "not authenticated"})
return True
if mtype == "register":
peer_id = peer_key(msg.get("peerId", ""))
network_id = (msg.get("networkId") or "").lower()
if not peer_id:
await send_json(ws, {"type": "error", "error": "peerId required"})
return True
peers[peer_id] = {"ws": ws, "networkId": network_id, "registeredAt": time.time()}
state["peerId"] = peer_id
await send_json(ws, {"type": "registered", "peerId": peer_id})
return True
peer_id = state.get("peerId")
if not peer_id:
await send_json(ws, {"type": "error", "error": "register first"})
return True
if mtype == "call_offer":
target_id = peer_key(msg.get("targetId", ""))
call_type = msg.get("callType") or "voice"
sdp = msg.get("sdp", "")
network_id = (msg.get("networkId") or peers[peer_id].get("networkId") or "").lower()
if not target_id or not sdp:
await send_json(ws, {"type": "error", "error": "targetId and sdp required"})
return True
call_id = new_call_id()
call = {
"callId": call_id,
"callType": call_type,
"networkId": network_id,
"callerId": peer_id,
"calleeId": target_id,
"state": "ringing",
"offerSdp": sdp,
"answerSdp": "",
"createdAt": time.time()
}
calls[call_id] = call
ice_queues[call_id] = []
track_call_for_peer(peer_id, call_id)
track_call_for_peer(target_id, call_id)
delivered = await push_to_peer(target_id, {
"type": "incoming_call",
"callId": call_id,
"callType": call_type,
"networkId": network_id,
"callerId": peer_id,
"callerName": msg.get("callerName", peer_id),
"sdp": sdp,
"overlayIps": msg.get("overlayIps", [])
})
await send_json(ws, {
"type": "call_state",
"callId": call_id,
"state": "ringing",
"delivered": delivered
})
return True
if mtype == "call_answer":
call_id = msg.get("callId", "")
sdp = msg.get("sdp", "")
call = calls.get(call_id)
if not call or peer_key(call["calleeId"]) != peer_id:
await send_json(ws, {"type": "error", "error": "call not found"})
return True
call["answerSdp"] = sdp
call["state"] = "accepted"
await push_to_peer(call["callerId"], {
"type": "call_state",
"callId": call_id,
"state": "accepted",
"sdp": sdp
})
await send_json(ws, {"type": "call_state", "callId": call_id, "state": "accepted"})
return True
if mtype == "call_reject":
call_id = msg.get("callId", "")
call = calls.get(call_id)
if call and peer_key(call["calleeId"]) == peer_id:
await push_to_peer(call["callerId"], {
"type": "call_state",
"callId": call_id,
"state": "rejected"
})
purge_call(call_id)
return True
if mtype == "call_end":
call_id = msg.get("callId", "")
call = calls.get(call_id)
if call and peer_id in (peer_key(call["callerId"]), peer_key(call["calleeId"])):
other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
await push_to_peer(other, {
"type": "call_state",
"callId": call_id,
"state": "ended"
})
purge_call(call_id)
return True
if mtype == "ice_candidate":
call_id = msg.get("callId", "")
call = calls.get(call_id)
if not call:
return True
entry = {
"from": peer_id,
"candidate": msg.get("candidate"),
"sdpMid": msg.get("sdpMid"),
"sdpMLineIndex": msg.get("sdpMLineIndex"),
"at": time.time()
}
ice_queues.setdefault(call_id, []).append(entry)
other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
await push_to_peer(other, {
"type": "ice_candidate",
"callId": call_id,
**{k: entry[k] for k in ("candidate", "sdpMid", "sdpMLineIndex")}
})
return True
if mtype == "ice_fetch":
call_id = msg.get("callId", "")
since = float(msg.get("since", 0))
queued = [e for e in ice_queues.get(call_id, []) if e.get("at", 0) > since and e.get("from") != peer_id]
await send_json(ws, {"type": "ice_candidates", "callId": call_id, "candidates": queued})
return True
if mtype == "chat_message":
target_id = peer_key(msg.get("targetId", ""))
if target_id:
await push_to_peer(target_id, {
"type": "chat_message",
"networkId": msg.get("networkId", ""),
"senderId": peer_id,
"senderName": msg.get("senderName", peer_id),
"body": msg.get("body", ""),
"at": int(time.time() * 1000)
})
return True
if mtype == "typing":
target_id = peer_key(msg.get("targetId", ""))
if target_id:
await push_to_peer(target_id, {
"type": "typing",
"networkId": msg.get("networkId", ""),
"senderId": peer_id,
"typing": bool(msg.get("typing", True))
})
return True
await send_json(ws, {"type": "error", "error": f"unknown type: {mtype}"})
return True
async def connection_handler(ws, path):
state = {"authed": False, "peerId": None}
query = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)
if "auth" in query and valid_token(query["auth"][0]):
state["authed"] = True
await send_json(ws, {"type": "auth_ok"})
try:
async for raw in ws:
try:
msg = json.loads(raw)
except json.JSONDecodeError:
await send_json(ws, {"type": "error", "error": "invalid json"})
continue
if not await handle_message(ws, state, msg):
break
finally:
pid = state.get("peerId")
if pid and peers.get(pid, {}).get("ws") is ws:
del peers[pid]
end_calls_for_peer(pid)
async def main():
port = load_port()
host = "127.0.0.1"
asyncio.create_task(ring_timeout_watch())
async with serve(connection_handler, host, port, ping_interval=20, ping_timeout=20):
print(f"BackOne WebHub listening on ws://{host}:{port}", flush=True)
await asyncio.Future()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=BackOne WebSocket signaling hub (webhub)
After=network.target backone.service
Wants=backone.service
[Service]
Type=simple
ExecStart=/usr/lib/backone/addons/webhub/backone-webhub
Restart=always
RestartSec=3
User=backone
Group=backone
[Install]
WantedBy=multi-user.target
+57
View File
@@ -0,0 +1,57 @@
window.BackOneRingingPopup = {
_audio: null,
showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
const el = document.getElementById('ringing-popup');
if (!el) return;
el.classList.remove('ringing-hidden');
el.innerHTML = `
<div class="ringing-card">
<h2>Incoming ${callType} call</h2>
<p>${callerName || 'Unknown peer'}</p>
<button class="ringing-accept">Accept</button>
<button class="ringing-reject">Reject</button>
</div>`;
el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
this.playRingtone();
if (Notification.permission === 'default') Notification.requestPermission();
if (Notification.permission === 'granted') {
new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
}
},
dismiss() {
const el = document.getElementById('ringing-popup');
if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
this.stopRingtone();
},
playRingtone() {
this.stopRingtone();
try {
this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
this._audio.loop = true;
this._audio.play().catch(() => {});
} catch (_) {}
},
stopRingtone() {
if (this._audio) { this._audio.pause(); this._audio = null; }
}
};
async function loadAddonManifest() {
const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
if (!res.ok) {
console.warn('manifest.json fetch failed:', res.status);
return [];
}
const data = await res.json();
return Array.isArray(data) ? data : [];
}
async function loadAddonModule(entry) {
const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
await import(entryPath);
const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
return factory ? factory() : null;
}
window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
+195
View File
@@ -0,0 +1,195 @@
/**
* BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
*/
window.BackOneAuth = (function () {
const LS_KEY = 'backone.authToken';
let cache = null;
function trimToken(s) {
return String(s || '').replace(/\s+/g, '');
}
function tokenFromUrl() {
try {
const params = new URLSearchParams(location.search);
const fromQuery = trimToken(params.get('auth') || params.get('token'));
if (fromQuery) return fromQuery;
const hash = (location.hash || '').replace(/^#\/?/, '');
const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
return trimToken(hashParams.get('auth') || hashParams.get('token'));
} catch (_) {
return '';
}
}
async function probeToken(token) {
if (!token) return false;
try {
const res = await fetch('/network', {
cache: 'no-store',
headers: { 'X-ZT1-Auth': token }
});
return res.ok;
} catch (_) {
return false;
}
}
async function fetchAuthtoken() {
for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
try {
const res = await fetch(path, { cache: 'no-store' });
if (res.ok) {
const data = await res.json();
const token = trimToken(data && data.token);
if (token) return token;
}
} catch (_) {
/* try next */
}
}
return '';
}
async function resolveToken() {
const urlToken = tokenFromUrl();
if (urlToken) {
if (await probeToken(urlToken)) {
return urlToken;
}
}
const stored = trimToken(localStorage.getItem(LS_KEY));
if (stored && await probeToken(stored)) {
return stored;
}
const apiToken = await fetchAuthtoken();
if (apiToken && await probeToken(apiToken)) {
return apiToken;
}
if (stored) return stored;
if (apiToken) return apiToken;
if (urlToken) return urlToken;
return '';
}
async function getToken() {
if (cache) return cache;
cache = await resolveToken();
return cache || '';
}
async function authHeaders(extra) {
const token = await getToken();
const headers = Object.assign({}, extra || {});
if (token) {
headers['X-ZT1-Auth'] = token;
}
return headers;
}
async function fetchWithAuth(path, options) {
options = options || {};
const headers = await authHeaders(options.headers || {});
return fetch(path, Object.assign({}, options, { headers }));
}
async function apiGet(path) {
const res = await fetchWithAuth(path, { cache: 'no-store' });
if (!res.ok) throw new Error(`${path} returned ${res.status}`);
return res.json();
}
async function apiGetFirst(paths) {
let lastErr = null;
for (const path of paths) {
try {
return await apiGet(path);
} catch (err) {
lastErr = err;
}
}
throw lastErr || new Error('API request failed');
}
async function apiSend(path, method, body) {
const headers = await authHeaders(
body != null ? { 'Content-Type': 'application/json' } : {}
);
const res = await fetch(path, {
method,
headers,
body: body != null ? JSON.stringify(body) : undefined
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(text || `${method} ${path} returned ${res.status}`);
}
if (res.status === 204) {
return { result: true };
}
const text = await res.text();
if (!text) {
return { result: true };
}
try {
return JSON.parse(text);
} catch (_) {
return { result: true };
}
}
async function apiSendFirst(paths, method, body) {
let lastErr = null;
for (const path of paths) {
try {
return await apiSend(path, method, body);
} catch (err) {
lastErr = err;
}
}
throw lastErr || new Error(`${method} failed`);
}
/** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
async function setUserToken(token) {
cache = trimToken(token);
if (cache) {
localStorage.setItem(LS_KEY, cache);
} else {
localStorage.removeItem(LS_KEY);
}
return cache;
}
async function validateCurrentToken() {
cache = null;
const token = await getToken();
if (!token) return false;
return probeToken(token);
}
function invalidate() {
cache = null;
}
async function init() {
return getToken();
}
return {
getToken,
authHeaders,
fetchWithAuth,
apiGet,
apiGetFirst,
apiSend,
apiSendFirst,
setUserToken,
validateCurrentToken,
invalidate,
init
};
})();
+57
View File
@@ -0,0 +1,57 @@
window.BackOneContacts = (function () {
let cache = null;
async function load() {
if (cache) return cache;
try {
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
.catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
if (!res.ok) {
cache = {};
return cache;
}
cache = await res.json();
if (!cache || typeof cache !== 'object') cache = {};
} catch (_) {
cache = {};
}
return cache;
}
function getDisplayName(peerId, controllerName) {
const c = cache || {};
const entry = c[peerId];
if (entry && entry.nickname) return entry.nickname;
if (controllerName) return controllerName;
return peerId;
}
function getNickname(peerId) {
const entry = (cache || {})[peerId];
return entry && entry.nickname ? entry.nickname : '';
}
async function setNickname(peerId, nickname) {
await load();
const patch = {};
patch[peerId] = { nickname: nickname || '' };
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}));
if (!res.ok) throw new Error('Failed to save nickname');
cache = await res.json();
return cache;
}
function invalidate() {
cache = null;
}
return { load, getDisplayName, getNickname, setNickname, invalidate };
})();
+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BackOne</title>
<link rel="stylesheet" href="/app/styles.css">
</head>
<body>
<header class="app-header">
<div class="app-brand">
<h1>BackOne</h1>
<span id="core-status-badge" class="core-badge offline">Offline</span>
</div>
<div id="network-bar" class="network-bar">
<label class="network-bar-label">Network
<select id="global-network"></select>
</label>
<input id="global-join-id" type="text" maxlength="16" placeholder="Join network ID" autocomplete="off" />
<button type="button" id="global-join-btn">Join</button>
<button type="button" id="global-leave-btn">Leave</button>
<span id="global-network-status" class="network-bar-status" aria-live="polite"></span>
<div id="auth-token-bar" class="auth-token-bar auth-token-hidden">
<label class="auth-token-label">Auth token
<input id="auth-token-input" type="password" autocomplete="off"
placeholder="Paste from ~/.backOneOneAuthToken" />
</label>
<button type="button" id="auth-token-save">Save</button>
</div>
</div>
<nav id="addon-nav" class="addon-nav"></nav>
</header>
<main id="addon-panel" class="addon-panel"></main>
<div id="ringing-popup" class="ringing-hidden"></div>
<script src="/app/auth-store.js"></script>
<script src="/app/contacts-store.js"></script>
<script src="/app/peer-directory.js"></script>
<script src="/app/network-session.js"></script>
<script src="/app/webhub-client.js"></script>
<script src="/app/webrtc-session.js"></script>
<script src="/app/peer-picker.js"></script>
<script src="/app/addon-loader.js"></script>
<script src="/app/main.js"></script>
</body>
</html>
+265
View File
@@ -0,0 +1,265 @@
window.BackOneAddons = window.BackOneAddons || {};
const addonButtons = {};
let addonModules = {};
let coreRefreshTimer = null;
function hashAddonId() {
const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
if (h === 'voice' || h === 'video' || h === 'chat') return h;
return '';
}
function activateAddon(id) {
const btn = addonButtons[id];
if (!btn) return;
btn.click();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function updateCoreBadge(statusEl) {
const badge = document.getElementById('core-status-badge');
const core = BackOneNetworkSession.getCoreStatus();
if (badge) {
badge.textContent = core.online ? 'Online' : 'Offline';
badge.classList.toggle('online', core.online);
badge.classList.toggle('offline', !core.online);
}
if (!statusEl) return;
if (!core.online) {
statusEl.textContent = 'Start the backone service to use networks and peers';
return;
}
const net = BackOneNetworkSession.getActiveNetwork();
const netPart = net ? networkLabel(net) : 'no network joined';
statusEl.textContent = `${netPart} · ${core.leafPeerCount} peer(s)`;
}
async function initAuthTokenBar(statusEl, onSaved) {
const bar = document.getElementById('auth-token-bar');
const input = document.getElementById('auth-token-input');
const saveBtn = document.getElementById('auth-token-save');
if (!bar || !input || !saveBtn) return;
async function refreshAuthUi() {
const ok = await BackOneAuth.validateCurrentToken();
bar.classList.toggle('auth-token-hidden', ok);
if (!ok && !input.value) {
input.placeholder = 'Paste from ~/.backOneOneAuthToken';
}
return ok;
}
saveBtn.addEventListener('click', async () => {
const token = input.value.trim();
if (!token) {
if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
return;
}
saveBtn.disabled = true;
await BackOneAuth.setUserToken(token);
BackOneAuth.invalidate();
const ok = await BackOneAuth.validateCurrentToken();
if (!ok) {
if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken';
saveBtn.disabled = false;
return;
}
input.value = '';
bar.classList.add('auth-token-hidden');
if (typeof onSaved === 'function') await onSaved();
saveBtn.disabled = false;
});
await refreshAuthUi();
return refreshAuthUi;
}
async function initNetworkBar() {
const bar = document.getElementById('network-bar');
if (!bar) return;
const netSel = bar.querySelector('#global-network');
const joinInput = bar.querySelector('#global-join-id');
const joinBtn = bar.querySelector('#global-join-btn');
const leaveBtn = bar.querySelector('#global-leave-btn');
const statusEl = bar.querySelector('#global-network-status');
async function reloadNetworkList(selectId) {
await BackOneNetworkSession.syncFromCore();
const networks = await BackOneNetworkSession.loadNetworks();
netSel.innerHTML = networks.map(n =>
`<option value="${n.id}">${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || 'OK')}</option>`
).join('') || '<option value="">— no networks —</option>';
const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
await BackOneNetworkSession.selectNetwork(netSel.value);
} else if (networks.length === 1) {
netSel.value = networks[0].id;
await BackOneNetworkSession.selectNetwork(networks[0].id);
} else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
netSel.value = BackOneNetworkSession.getNetworkId();
}
updateCoreBadge(statusEl);
}
netSel.addEventListener('change', async () => {
if (!netSel.value) return;
statusEl.textContent = 'Switching network…';
await BackOneNetworkSession.selectNetwork(netSel.value);
await BackOneNetworkSession.syncFromCore();
updateCoreBadge(statusEl);
});
joinBtn.addEventListener('click', async () => {
const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
if (!id) {
statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
return;
}
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Joining ${id}…`;
try {
await BackOneNetworkSession.joinNetwork(id);
joinInput.value = '';
await reloadNetworkList(id);
} catch (err) {
statusEl.textContent = err.message || 'Join failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
leaveBtn.addEventListener('click', async () => {
const id = netSel.value || BackOneNetworkSession.getNetworkId();
if (!id) {
statusEl.textContent = 'Select a network to leave';
return;
}
if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Leaving ${id}…`;
try {
await BackOneNetworkSession.leaveNetwork(id);
await reloadNetworkList();
} catch (err) {
statusEl.textContent = err.message || 'Leave failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
BackOneNetworkSession.subscribe(() => updateCoreBadge(statusEl));
const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
await reloadNetworkList();
if (refreshAuthUi) await refreshAuthUi();
coreRefreshTimer = setInterval(() => {
BackOneNetworkSession.syncFromCore().then(() => updateCoreBadge(statusEl));
}, 45000);
}
async function initCommsStack() {
await BackOneWebHubClient.connect();
await BackOneWebRtcSession.init();
BackOneWebHubClient.subscribe('incoming_call', (msg) => {
if (BackOneWebRtcSession.isInCall()) return;
BackOneRingingPopup.showIncomingCall({
callerName: msg.callerName || msg.callerId,
callType: msg.callType || 'voice',
callId: msg.callId,
onAccept: async () => {
const type = msg.callType || 'voice';
const addonId = type === 'video' ? 'video' : 'voice';
if (addonButtons[addonId]) activateAddon(addonId);
try {
await BackOneWebRtcSession.acceptIncomingCall(msg);
} catch (err) {
alert(err.message || 'Could not accept call');
}
},
onReject: (callId) => {
BackOneWebRtcSession.rejectIncomingCall(callId || msg.callId);
}
});
});
}
async function initAppShell() {
await BackOneAuth.init();
await initNetworkBar();
await initCommsStack();
const nav = document.getElementById('addon-nav');
const panel = document.getElementById('addon-panel');
const addons = await BackOneAddonLoader.loadAddonManifest();
if (!addons.length) {
panel.innerHTML = '<p>No UI addons found. Install backone-addon-secure-chat, backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: <code>sudo backone-updater rebuild-manifest</code> and restart backone.</p>';
return;
}
const navOrder = ['secure-chat', 'chat', 'video', 'voice'];
const sorted = [...addons].sort((a, b) => {
const ia = navOrder.indexOf(a.id);
const ib = navOrder.indexOf(b.id);
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
});
for (const entry of sorted) {
const mod = await BackOneAddonLoader.loadAddonModule(entry);
if (mod) addonModules[entry.id] = mod;
const btn = document.createElement('button');
btn.dataset.addonId = entry.id;
btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
btn.onclick = () => {
nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
panel.innerHTML = '';
location.hash = entry.id;
if (addonModules[entry.id] && addonModules[entry.id].mount) {
addonModules[entry.id].mount(panel);
} else {
panel.innerHTML = `<p>Addon "${entry.id}" loaded.</p>`;
}
};
addonButtons[entry.id] = btn;
nav.appendChild(btn);
}
const fromHash = hashAddonId();
if (fromHash && addonButtons[fromHash]) {
activateAddon(fromHash);
} else {
nav.querySelector('button')?.click();
}
}
window.addEventListener('hashchange', () => {
const id = hashAddonId();
if (id) activateAddon(id);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
BackOneNetworkSession.syncFromCore();
}
});
document.addEventListener('DOMContentLoaded', initAppShell);
+178
View File
@@ -0,0 +1,178 @@
/**
* Single BackOne core network for all addons (same as backone-cli listnetworks/join).
*/
window.BackOneNetworkSession = (function () {
const NETWORK_KEY = 'backone.lastNetworkId';
const PEER_KEY = 'backone.lastPeerId';
let activeNetwork = null;
let selectedPeer = null;
let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
const listeners = new Set();
function emit() {
const snap = {
network: activeNetwork,
networkId: getNetworkId(),
peer: selectedPeer,
core: { ...coreStatus }
};
listeners.forEach(fn => {
try { fn(snap); } catch (_) { /* ignore */ }
});
}
function getNetworkId() {
return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
}
function setNetworkId(id) {
const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
if (nwid) {
localStorage.setItem(NETWORK_KEY, nwid);
} else {
localStorage.removeItem(NETWORK_KEY);
}
BackOnePeerDirectory.setLastNetworkId(nwid);
return nwid;
}
function getSelectedPeerId() {
return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
}
function setSelectedPeer(peer) {
selectedPeer = peer || null;
if (peer && peer.peerId) {
localStorage.setItem(PEER_KEY, peer.peerId);
} else {
localStorage.removeItem(PEER_KEY);
}
emit();
}
function pickDefaultNetworkId(networks) {
if (!networks.length) return '';
const stored = getNetworkId();
if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
return stored;
}
const preferred = networks.find(n => n.status === 'OK')
|| networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
|| networks[0];
return preferred ? (preferred.id || '').toLowerCase() : '';
}
/**
* On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
*/
async function syncFromCore() {
const token = await BackOneAuth.getToken();
const [status, networks, cliPeers] = await Promise.all([
BackOnePeerDirectory.loadCoreStatus(),
BackOnePeerDirectory.loadNetworks(),
BackOnePeerDirectory.loadCliPeers()
]);
coreStatus = {
online: !!(status && status.online),
address: (status && status.address) || '',
version: (status && status.version) || '',
networkCount: networks.length,
leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
authenticated: !!token
};
const pick = pickDefaultNetworkId(networks);
if (pick) {
setNetworkId(pick);
activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
|| await BackOnePeerDirectory.loadNetwork(pick);
} else {
setNetworkId('');
activeNetwork = null;
}
emit();
return { coreStatus, activeNetwork, networks };
}
async function refresh() {
const id = getNetworkId();
if (!id) {
activeNetwork = null;
emit();
return null;
}
activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
if (!activeNetwork) {
activeNetwork = { id, name: id, status: 'UNKNOWN' };
}
emit();
return activeNetwork;
}
async function loadNetworks() {
return BackOnePeerDirectory.loadNetworks();
}
async function selectNetwork(nwid) {
const id = setNetworkId(nwid);
if (!id) {
await refresh();
return null;
}
return refresh();
}
async function joinNetwork(nwid) {
const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
await BackOnePeerDirectory.joinNetwork(id);
return syncFromCore();
}
async function leaveNetwork(nwid) {
const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
if (!id) throw new Error('No network selected');
await BackOnePeerDirectory.leaveNetwork(id);
if (getNetworkId() === id) {
setNetworkId('');
selectedPeer = null;
localStorage.removeItem(PEER_KEY);
}
return syncFromCore();
}
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function requireNetworkId() {
const id = getNetworkId();
if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
return id;
}
function getCoreStatus() {
return { ...coreStatus };
}
return {
getNetworkId,
getActiveNetwork: () => activeNetwork,
getCoreStatus,
getSelectedPeer: () => selectedPeer,
getSelectedPeerId,
setSelectedPeer,
selectNetwork,
joinNetwork,
leaveNetwork,
loadNetworks,
syncFromCore,
refresh,
subscribe,
requireNetworkId
};
})();
+266
View File
@@ -0,0 +1,266 @@
window.BackOnePeerDirectory = (function () {
const NETWORK_KEY = 'backone.lastNetworkId';
const API = {
peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
networks: ['/api/backone/networks', '/app/api/networks', '/network'],
status: ['/api/backone/status', '/app/api/status', '/status'],
network: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
],
members: (id) => [
`/api/backone/network/${id}/members`,
`/app/api/network/${id}/members`,
`/unstable/controller/network/${id}/member`
],
join: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
],
leave: (id) => [
`/api/backone/network/${id}`,
`/app/api/network/${id}`,
`/network/${id}`
]
};
function apiGet(path) {
return BackOneAuth.apiGet(path);
}
/** Same data as `backone-cli -j peers` (GET /peer). */
async function loadCliPeers() {
try {
const peers = await BackOneAuth.apiGetFirst(API.peers);
return Array.isArray(peers) ? peers : [];
} catch (_) {
return [];
}
}
/** Same data as `backone-cli -j listnetworks` (GET /network). */
async function loadNetworks() {
try {
const nets = await BackOneAuth.apiGetFirst(API.networks);
return Array.isArray(nets) ? nets : [];
} catch (_) {
return [];
}
}
/** Same data as `backone-cli -j info` (GET /status). */
async function loadCoreStatus() {
try {
return await BackOneAuth.apiGetFirst(API.status);
} catch (_) {
try {
const res = await fetch('/health', { cache: 'no-store' });
return { online: res.ok };
} catch (_2) {
return { online: false };
}
}
}
function countLeafPeers(cliPeers) {
if (!Array.isArray(cliPeers)) return 0;
return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
}
async function loadNetwork(nwid) {
if (!nwid) return null;
const networks = await loadNetworks();
const found = networks.find(n => n.id === nwid);
if (found) return found;
try {
return await BackOneAuth.apiGetFirst(API.network(nwid));
} catch (_) {
return null;
}
}
function getLastNetworkId() {
return localStorage.getItem(NETWORK_KEY) || '';
}
function setLastNetworkId(nwid) {
if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
}
function peerIsOnline(p) {
if (!p) return false;
const paths = p.paths || [];
return paths.some(x => x.active && !x.expired);
}
function peerLinkLabel(p) {
if (!p) return '';
if (p.tunneled) return 'RELAY';
const paths = p.paths || [];
for (const path of paths) {
if (path.preferred && path.active && !path.expired) {
return 'DIRECT';
}
}
for (const path of paths) {
if (path.active && !path.expired) {
return 'DIRECT';
}
}
return 'RELAY';
}
function peerPreferredPath(p) {
if (!p) return '';
const paths = p.paths || [];
for (const path of paths) {
if (path.preferred && path.address) return path.address;
}
for (const path of paths) {
if (path.active && path.address) return path.address;
}
return '';
}
function cliPeerMap(cliPeers) {
const map = {};
for (const p of cliPeers) {
const id = (p.address || '').toLowerCase();
if (id) map[id] = p;
}
return map;
}
async function loadMemberMap(nwid) {
if (!nwid) return {};
try {
const data = await BackOneAuth.apiGetFirst(API.members(nwid));
const map = {};
if (data && Array.isArray(data.data)) {
for (const m of data.data) {
const id = (m.id || '').toLowerCase();
if (id) map[id] = m;
}
}
return map;
} catch (_) {
return {};
}
}
function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
const controllerName = member ? (member.name || '') : '';
const ips = member && member.ipAssignments ? member.ipAssignments : [];
return {
peerId,
networkId: networkId || '',
networkName: networkName || '',
displayName: BackOneContacts.getDisplayName(peerId, controllerName),
controllerName,
nickname: BackOneContacts.getNickname(peerId),
online: peerIsOnline(cliPeer),
latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
version: (cliPeer && cliPeer.version) || '',
link: cliPeer ? peerLinkLabel(cliPeer) : '',
path: cliPeer ? peerPreferredPath(cliPeer) : '',
ip: ips.length ? ips[0] : '',
onNetwork: !!member
};
}
async function loadPeersForNetwork(nwid) {
if (!nwid) return [];
const [cliPeers, members, selfStatus, network] = await Promise.all([
loadCliPeers(),
loadMemberMap(nwid),
loadCoreStatus().catch(() => ({})),
loadNetwork(nwid)
]);
await BackOneContacts.load();
const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
const networkId = nwid;
const networkName = network ? (network.name || network.id || nwid) : nwid;
const cliById = cliPeerMap(cliPeers);
const out = [];
if (Object.keys(members).length > 0) {
for (const peerId of Object.keys(members)) {
const member = members[peerId];
if (!member || member.authorized === false) continue;
if (peerId === selfId) continue;
out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
}
} else {
for (const peerId of Object.keys(cliById)) {
const p = cliById[peerId];
if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
if (p.role && p.role !== 'LEAF') continue;
out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
}
}
out.sort((a, b) => {
if (a.online !== b.online) return a.online ? -1 : 1;
return a.displayName.localeCompare(b.displayName);
});
return out;
}
function searchPeers(query, list) {
const q = (query || '').trim().toLowerCase();
if (!q) return list;
return list.filter(p =>
p.displayName.toLowerCase().includes(q) ||
p.peerId.includes(q) ||
(p.networkName || '').toLowerCase().includes(q) ||
(p.networkId || '').includes(q) ||
(p.controllerName || '').toLowerCase().includes(q) ||
(p.role || '').toLowerCase().includes(q) ||
(p.path || '').toLowerCase().includes(q) ||
(p.ip || '').toLowerCase().includes(q)
);
}
function normalizeNetworkId(input) {
const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
return id.length === 16 ? id : '';
}
async function joinNetwork(nwid) {
const id = normalizeNetworkId(nwid);
if (!id) {
throw new Error('Network ID must be 16 hexadecimal characters');
}
return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
}
async function leaveNetwork(nwid) {
const id = normalizeNetworkId(nwid);
if (!id) {
throw new Error('Network ID must be 16 hexadecimal characters');
}
return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
}
return {
loadCliPeers,
loadNetworks,
loadNetwork,
loadCoreStatus,
countLeafPeers,
loadPeersForNetwork,
getLastNetworkId,
setLastNetworkId,
joinNetwork,
leaveNetwork,
normalizeNetworkId,
searchPeers,
peerIsOnline,
peerLinkLabel
};
})();
+193
View File
@@ -0,0 +1,193 @@
window.BackOnePeerPicker = function PeerPicker(container, options) {
options = options || {};
const onSelect = options.onSelect || (() => {});
const onNetworkChange = options.onNetworkChange || (() => {});
const onAction = options.onAction || null;
const actionLabel = options.actionLabel || 'Select';
const showActionButton = options.showActionButton !== false;
const useSharedNetwork = options.useSharedNetwork !== false;
const layout = options.layout || 'default';
let selectedPeer = null;
let peers = [];
let currentNetwork = null;
let unsub = null;
if (layout === 'sidebar') {
container.innerHTML = `
<div class="peer-picker">
<div id="pp-network-banner" class="peer-info-box"></div>
<label class="peer-picker-row">
<input id="pp-search" type="search" placeholder="Name or peer ID" />
</label>
<ul id="pp-list" class="peer-list"></ul>
</div>`;
} else {
container.innerHTML = `
<div class="peer-picker">
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
<div id="pp-network-banner" class="peer-network-banner"></div>
<label class="peer-picker-row">Search
<input id="pp-search" type="search" placeholder="Name or peer ID" />
</label>
<ul id="pp-list" class="peer-list"></ul>
<div id="pp-selected" class="peer-selected"></div>
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
</div>`;
}
const netBanner = container.querySelector('#pp-network-banner');
const searchInput = container.querySelector('#pp-search');
const listEl = container.querySelector('#pp-list');
const selectedEl = container.querySelector('#pp-selected');
const actionBtn = container.querySelector('#pp-action');
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function renderNetworkBanner() {
if (!useSharedNetwork) return;
const id = BackOneNetworkSession.getNetworkId();
const core = BackOneNetworkSession.getCoreStatus();
if (!core.online) {
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers unavailable</span>';
return;
}
if (!currentNetwork || !id) {
netBanner.innerHTML = '<span class="peer-network-warn">No network — join one in the header bar</span>';
return;
}
const labelSuffix = `${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
if (layout === 'sidebar') {
netBanner.textContent = `${networkLabel(currentNetwork)} · ${labelSuffix}`;
} else {
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${textSuffix}`;
}
}
function renderList() {
const q = searchInput.value;
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
const netTag = currentNetwork ? (currentNetwork.name || currentNetwork.id) : '';
listEl.innerHTML = filtered.map(p => {
const lat = p.latency != null ? `${p.latency}ms` : '—';
const link = p.link || 'DIRECT';
const meta = [p.role || 'LEAF', lat, link, netTag].filter(Boolean).join(' · ');
return `
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
<span class="peer-main">
<strong class="peer-name">${escapeHtml(p.displayName || p.peerId)}</strong>
<small class="peer-sub">${escapeHtml(meta)}</small>
</span>
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
</li>`;
}).join('') || '<li class="peer-empty">No peers on this network</li>';
listEl.querySelectorAll('.peer-item').forEach(li => {
li.addEventListener('click', (ev) => {
if (ev.target.classList.contains('peer-edit')) return;
const id = li.dataset.id;
selectedPeer = peers.find(x => x.peerId === id) || null;
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
renderList();
renderSelected();
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
});
const editBtn = li.querySelector('.peer-edit');
if (editBtn) {
editBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const id = li.dataset.id;
const peer = peers.find(x => x.peerId === id);
if (!peer) return;
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
if (nick === null) return;
await BackOneContacts.setNickname(peer.peerId, nick);
BackOneContacts.invalidate();
await refreshPeers();
});
}
});
}
function renderSelected() {
if (layout === 'sidebar') return;
if (!selectedPeer) {
if (selectedEl) selectedEl.textContent = '';
if (actionBtn) actionBtn.disabled = true;
return;
}
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
if (selectedEl) {
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
}
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function refreshPeers() {
currentNetwork = BackOneNetworkSession.getActiveNetwork();
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
currentNetwork = await BackOneNetworkSession.refresh();
}
renderNetworkBanner();
const nwid = BackOneNetworkSession.getNetworkId();
await BackOneContacts.load();
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
if (lastPeerId) {
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
}
renderList();
renderSelected();
onNetworkChange(currentNetwork, peers);
}
searchInput.addEventListener('input', renderList);
if (actionBtn) {
actionBtn.addEventListener('click', () => {
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
});
}
if (useSharedNetwork) {
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
}
refreshPeers();
return {
getSelectedPeer: () => selectedPeer,
getSelectedNetwork: () => currentNetwork,
getPeers: () => peers,
refresh: refreshPeers,
destroy: () => { if (unsub) unsub(); }
};
};
window.BackOneCommLayout = {
connectionBarHtml(peer, network) {
const netLabel = network
? (network.name ? `${network.name} (${network.id})` : network.id)
: BackOneNetworkSession.getNetworkId() || '—';
const lat = peer && peer.latency != null ? `${peer.latency}ms` : '—';
const latClass = peer && peer.latency != null && peer.latency < 100 ? 'latency-ok' : '';
return `
<div class="comm-stat"><label>Network ID</label><span>${escapeHtml(netLabel)}</span></div>
<div class="comm-stat"><label>Peer ID</label><span>${peer ? escapeHtml(peer.peerId) : '—'}</span></div>
<div class="comm-stat"><label>Role</label><span>${peer ? escapeHtml(peer.role || 'LEAF') : '—'}</span></div>
<div class="comm-stat"><label>Transport</label><span>${peer ? escapeHtml(peer.link || 'DIRECT') : '—'}</span></div>
<div class="comm-stat"><label>Latency</label><span class="${latClass}">${lat}</span></div>
<div class="comm-stat"><label>Quality</label><span class="comm-quality"><span></span><span></span><span></span><span></span></span></div>`;
}
};
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
+944
View File
@@ -0,0 +1,944 @@
:root {
--bg: #0f1419;
--surface: #1a2332;
--surface-2: #243044;
--border: #2d3a4d;
--text: #e7ecf1;
--muted: #8b9bb4;
--accent: #4a90d9;
--accent-dim: #2d5a8a;
--ok: #2ecc71;
--warn: #e6a23c;
--danger: #e74c3c;
}
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.65rem 1rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.app-brand {
display: flex;
align-items: center;
gap: 0.6rem;
}
.app-brand h1 {
margin: 0;
font-size: 1.15rem;
font-weight: 600;
}
.core-badge {
font-size: 0.75rem;
padding: 0.15rem 0.55rem;
border-radius: 999px;
font-weight: 500;
}
.core-badge.online {
background: rgba(46, 204, 113, 0.15);
color: var(--ok);
border: 1px solid rgba(46, 204, 113, 0.35);
}
.core-badge.offline {
background: rgba(231, 76, 60, 0.12);
color: var(--danger);
border: 1px solid rgba(231, 76, 60, 0.3);
}
.network-bar {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
flex: 1;
min-width: 240px;
}
.network-bar-label { font-size: 0.82rem; color: var(--muted); }
.network-bar select,
.network-bar input,
.auth-token-bar input {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.35rem 0.5rem;
font-size: 0.85rem;
}
.network-bar input { width: 9.5rem; }
.network-bar button,
.auth-token-bar button {
background: var(--accent-dim);
color: #fff;
border: none;
padding: 0.35rem 0.65rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.network-bar-status {
font-size: 0.75rem;
color: #6b8cae;
flex: 1 1 100%;
}
.auth-token-bar {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1 1 100%;
}
.auth-token-hidden { display: none; }
.auth-token-label { font-size: 0.82rem; color: var(--muted); }
.auth-token-bar input { width: 14rem; }
.addon-nav {
display: flex;
gap: 0.35rem;
margin-left: auto;
}
.addon-nav button {
background: transparent;
color: var(--muted);
border: 1px solid transparent;
padding: 0.4rem 0.85rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
}
.addon-nav button:hover { color: var(--text); background: var(--surface-2); }
.addon-nav button.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.addon-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.addon-panel > p {
padding: 1.5rem;
margin: 0;
}
/* —— comm layout (voice / video / chat) —— */
.comm-layout {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.comm-sidebar {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: var(--bg);
min-height: 0;
}
/* Chat & voice: 25% contact list / 75% main panel */
.comm-layout.comm-split-layout .comm-sidebar {
width: 25%;
max-width: 25%;
flex: 0 0 25%;
min-width: 200px;
}
.comm-layout.comm-split-layout .comm-main {
flex: 1 1 75%;
width: 75%;
max-width: 75%;
min-width: 0;
}
.comm-layout.comm-split-layout .comm-sidebar-head {
padding: 0.75rem 0.75rem 0.4rem;
}
.comm-layout.comm-split-layout .comm-sidebar-head h2 {
font-size: 0.95rem;
}
.comm-layout.comm-split-layout .comm-sidebar-body {
padding: 0.5rem 0.65rem;
overflow: hidden;
}
.comm-layout.comm-split-layout .comm-sidebar-actions {
padding: 0.5rem 0.65rem 0.75rem;
}
.comm-sidebar-head {
padding: 1rem 1rem 0.5rem;
border-bottom: 1px solid var(--border);
}
.comm-sidebar-head h2 {
margin: 0 0 0.35rem;
font-size: 1.05rem;
font-weight: 600;
}
.comm-sidebar-sub {
margin: 0;
font-size: 0.72rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1.4;
}
.comm-sidebar-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
}
.comm-sidebar-actions {
padding: 0.75rem 1rem 1rem;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.comm-selected-label {
font-size: 0.78rem;
color: var(--muted);
margin-bottom: 0.15rem;
}
.comm-selected-label strong {
color: var(--text);
font-family: ui-monospace, monospace;
}
.comm-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
background: #0a0e14;
}
.comm-main-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--surface);
font-size: 0.85rem;
}
.comm-breadcrumb {
color: var(--muted);
}
.comm-breadcrumb strong { color: var(--text); }
.comm-head-status {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--muted);
font-size: 0.82rem;
}
.comm-head-status .connected {
color: var(--ok);
font-weight: 500;
}
.comm-stage {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
background: #000;
}
.comm-stage video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.comm-remote-placeholder {
text-align: center;
color: var(--muted);
}
.comm-remote-placeholder .avatar {
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--surface-2);
margin: 0 auto 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: #5a6a7d;
}
.comm-remote-placeholder .peer-id {
font-family: ui-monospace, monospace;
color: var(--text);
font-size: 0.95rem;
}
.comm-remote-tag {
position: absolute;
top: 12px;
left: 12px;
background: rgba(0, 0, 0, 0.55);
padding: 0.2rem 0.55rem;
border-radius: 4px;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.comm-remote-tag .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
.comm-call-controls {
position: absolute;
bottom: 1.25rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.5rem;
padding: 0.45rem;
background: rgba(26, 35, 50, 0.92);
border-radius: 10px;
border: 1px solid var(--border);
}
.comm-call-controls button {
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--border);
padding: 0.45rem 0.85rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
}
.comm-call-controls button.danger {
background: var(--danger);
border-color: var(--danger);
color: #fff;
}
.comm-connection-bar {
display: flex;
flex-wrap: wrap;
gap: 1.25rem 2rem;
padding: 0.65rem 1rem;
background: var(--surface);
border-top: 1px solid var(--border);
font-size: 0.78rem;
}
.comm-stat label {
display: block;
color: var(--muted);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.15rem;
}
.comm-stat span {
font-family: ui-monospace, monospace;
font-size: 0.82rem;
}
.comm-stat .latency-ok { color: var(--ok); }
.comm-quality {
display: inline-flex;
align-items: flex-end;
gap: 2px;
height: 14px;
}
.comm-quality span {
width: 3px;
background: var(--ok);
border-radius: 1px;
}
.comm-quality span:nth-child(1) { height: 4px; }
.comm-quality span:nth-child(2) { height: 7px; }
.comm-quality span:nth-child(3) { height: 10px; }
.comm-quality span:nth-child(4) { height: 14px; }
/* —— peer picker —— */
.peer-picker { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.peer-info-box {
font-size: 0.82rem;
color: #9eb4d0;
margin-bottom: 0.75rem;
padding: 0.55rem 0.65rem;
background: rgba(45, 90, 138, 0.25);
border: 1px solid rgba(74, 144, 217, 0.25);
border-radius: 6px;
line-height: 1.35;
}
.peer-picker-row { display: block; margin-bottom: 0.65rem; }
.peer-picker-row input {
width: 100%;
margin-top: 0.25rem;
padding: 0.45rem 0.55rem;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.85rem;
}
.peer-list {
list-style: none;
padding: 0;
margin: 0;
--peer-row-height: 3.25rem;
max-height: calc(var(--peer-row-height) * 10);
flex: 0 1 auto;
overflow-x: hidden;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
scrollbar-gutter: stable;
}
.peer-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
min-height: var(--peer-row-height);
padding: 0.55rem 0.65rem;
cursor: pointer;
border-bottom: 1px solid var(--border);
box-sizing: border-box;
}
.peer-item:last-child { border-bottom: none; }
.peer-item:hover,
.peer-item.active {
background: var(--surface-2);
}
.peer-item.active { border-left: 3px solid var(--accent); padding-left: calc(0.65rem - 3px); }
.peer-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
flex-shrink: 0;
margin-top: 0.35rem;
}
.peer-item.online .peer-dot { background: var(--ok); }
.peer-main { flex: 1; min-width: 0; }
.peer-name {
display: block;
font-family: ui-monospace, monospace;
font-size: 0.88rem;
}
.peer-sub {
color: var(--muted);
font-size: 0.72rem;
display: block;
margin-top: 0.15rem;
}
.peer-edit {
background: transparent;
border: none;
color: var(--muted);
cursor: pointer;
padding: 0.2rem;
font-size: 0.85rem;
}
.peer-empty { padding: 1rem; color: var(--muted); font-size: 0.85rem; }
.peer-network-warn { color: var(--warn); }
.btn-primary {
background: var(--accent);
color: #fff;
border: none;
padding: 0.55rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
font-weight: 500;
}
.btn-primary:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-secondary {
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--border);
padding: 0.55rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
}
/* —— chat —— */
.chat-thread-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.comm-split-layout .chat-thread-panel {
padding: 0;
}
.chat-thread-header {
flex-shrink: 0;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
margin: 0;
font-size: 0.9rem;
background: var(--surface);
}
.chat-thread-header small {
color: var(--muted);
font-weight: normal;
margin-left: 0.35rem;
}
.chat-messages,
#chat-thread {
flex: 1;
overflow: auto;
padding: 1rem;
margin: 0;
border: none;
border-radius: 0;
background: #0a0e14;
}
.chat-empty {
color: var(--muted);
text-align: center;
margin: 2rem 0;
font-size: 0.88rem;
}
.chat-message {
margin-bottom: 1rem;
max-width: 85%;
}
.chat-message-meta {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.chat-message-sender {
color: #e8a54b;
font-weight: 600;
font-size: 0.88rem;
}
.chat-message-time {
color: var(--muted);
font-size: 0.72rem;
}
.chat-message-body {
color: var(--text);
font-size: 0.9rem;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
}
.chat-message-file {
margin-top: 0.35rem;
font-size: 0.85rem;
color: var(--accent);
}
.chat-compose {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-top: 1px solid var(--border);
background: var(--surface);
}
.chat-compose textarea {
width: 100%;
padding: 0.5rem;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
resize: vertical;
}
.chat-compose-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* —— voice —— */
.voice-main-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: #0a0e14;
}
.voice-active-banner {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 1rem;
background: rgba(45, 90, 138, 0.35);
border-bottom: 1px solid var(--border);
font-size: 0.88rem;
}
.voice-active-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ok);
margin-right: 0.4rem;
}
.voice-active-controls {
display: flex;
gap: 0.5rem;
}
.voice-active-controls button {
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--border);
padding: 0.35rem 0.75rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
}
.voice-active-controls button.danger {
background: var(--danger);
border-color: var(--danger);
color: #fff;
}
.voice-session-hint {
margin: 1rem;
color: var(--text-muted);
font-size: 0.9rem;
}
.voice-remote-audio {
display: none;
}
.voice-history-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.voice-history-title {
margin: 0;
padding: 0.75rem 1rem;
font-size: 0.9rem;
font-weight: 600;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
background: var(--surface);
}
.voice-call-history {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
overflow-y: auto;
}
.voice-call-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.voice-call-item:hover {
background: var(--surface-2);
}
.voice-call-icon {
width: 1.75rem;
text-align: center;
font-size: 1rem;
color: var(--accent);
flex-shrink: 0;
}
.voice-call-main {
flex: 1;
min-width: 0;
}
.voice-call-name {
display: block;
font-size: 0.9rem;
font-family: ui-monospace, monospace;
}
.voice-call-sub {
color: var(--muted);
font-size: 0.75rem;
}
.voice-call-when {
flex-shrink: 0;
color: var(--muted);
font-size: 0.75rem;
}
.voice-call-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--muted);
font-size: 0.88rem;
}
/* legacy voice stage (unused) */
.voice-stage-inner {
text-align: center;
padding: 2rem;
}
.voice-stage-inner .avatar {
width: 96px;
height: 96px;
border-radius: 50%;
background: var(--surface-2);
margin: 0 auto 1rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
}
/* —— ringing popup —— */
.ringing-hidden { display: none; }
#ringing-popup {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
#ringing-popup.ringing-hidden { display: none; }
.ringing-card {
background: var(--surface);
padding: 2rem;
border-radius: 12px;
text-align: center;
min-width: 280px;
}
.ringing-card button {
margin: 0.5rem;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 6px;
cursor: pointer;
}
.ringing-accept { background: var(--ok); color: #000; }
.ringing-reject { background: var(--danger); color: #fff; }
/* —— video call: remote fills main stage, local is PiP —— */
.comm-layout.video-in-call .comm-sidebar {
display: none;
}
.comm-layout.video-in-call .comm-main-head {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 4;
background: linear-gradient(to bottom, rgba(10, 14, 20, 0.85), transparent);
border-bottom: none;
}
.video-stage .video-remote-main {
position: absolute;
inset: 0;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
background: #000;
}
.video-stage .video-remote-stream {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.25s ease;
}
.video-stage .video-remote-stream.active {
opacity: 1;
}
.video-stage.has-remote .comm-remote-placeholder {
display: none;
}
.video-stage .comm-remote-tag {
z-index: 3;
}
.video-local-pip {
position: absolute;
bottom: 4.5rem;
right: 1rem;
width: 180px;
z-index: 3;
border-radius: 8px;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.2);
background: #000;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
}
.video-pip-label {
position: absolute;
top: 6px;
left: 8px;
z-index: 1;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.85);
background: rgba(0, 0, 0, 0.5);
padding: 0.1rem 0.35rem;
border-radius: 3px;
}
.video-local-pip video {
width: 100%;
height: auto;
display: block;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.video-stage .comm-call-controls {
z-index: 4;
}
+170
View File
@@ -0,0 +1,170 @@
/**
* WebSocket client for backone-webhub (127.0.0.1, authtoken auth).
*/
window.BackOneWebHubClient = (function () {
let ws = null;
let connected = false;
let reconnectDelay = 1000;
let reconnectTimer = null;
let pollTimer = null;
let port = 9994;
let selfPeerId = '';
const seenIncoming = new Set();
const handlers = new Map();
function emit(type, msg) {
const set = handlers.get(type);
if (!set) return;
set.forEach(fn => {
try { fn(msg); } catch (_) { /* ignore */ }
});
const all = handlers.get('*');
if (all) {
all.forEach(fn => {
try { fn(type, msg); } catch (_) { /* ignore */ }
});
}
}
function subscribe(type, fn) {
if (!handlers.has(type)) handlers.set(type, new Set());
handlers.get(type).add(fn);
return () => handlers.get(type)?.delete(fn);
}
function send(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
return true;
}
return false;
}
function registerPeer() {
const core = BackOneNetworkSession.getCoreStatus();
selfPeerId = ((core && core.address) || '').toLowerCase();
const networkId = BackOneNetworkSession.getNetworkId();
if (!selfPeerId) return;
send({ type: 'register', peerId: selfPeerId, networkId });
}
function stopHttpPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startHttpPoll() {
stopHttpPoll();
pollTimer = setInterval(async () => {
if (connected) return;
const peerId = selfPeerId || ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
if (!peerId) return;
try {
const res = await BackOneAuth.fetchWithAuth(
`/controller/comms/call/incoming?peerId=${encodeURIComponent(peerId)}`
);
if (!res.ok) return;
const data = await res.json();
const calls = Array.isArray(data.calls) ? data.calls : [];
for (const call of calls) {
if (seenIncoming.has(call.callId)) continue;
seenIncoming.add(call.callId);
emit('incoming_call', {
type: 'incoming_call',
callId: call.callId,
callType: call.callType || 'voice',
callerId: call.callerId,
callerName: call.callerName || call.callerId,
networkId: call.networkId,
sdp: call.sdp
});
}
} catch (_) { /* ignore */ }
}, 2000);
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connect();
}, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
}
async function loadPort() {
try {
const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
if (res.ok) {
const cfg = await res.json();
if (cfg.webhubPort) port = cfg.webhubPort;
}
} catch (_) { /* ignore */ }
}
async function connect() {
await loadPort();
const token = await BackOneAuth.getToken();
if (!token) return false;
if (ws) {
try { ws.close(); } catch (_) { /* ignore */ }
ws = null;
}
return new Promise((resolve) => {
const url = `ws://127.0.0.1:${port}/?auth=${encodeURIComponent(token)}`;
try {
ws = new WebSocket(url);
} catch (_) {
connected = false;
startHttpPoll();
scheduleReconnect();
resolve(false);
return;
}
ws.onopen = () => {
connected = true;
reconnectDelay = 1000;
stopHttpPoll();
send({ type: 'auth', token });
registerPeer();
resolve(true);
};
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg && msg.type) emit(msg.type, msg);
} catch (_) { /* ignore */ }
};
ws.onclose = () => {
connected = false;
ws = null;
startHttpPoll();
scheduleReconnect();
};
ws.onerror = () => {
connected = false;
};
});
}
BackOneNetworkSession.subscribe(() => {
if (connected) registerPeer();
});
return {
connect,
send,
subscribe,
isConnected: () => connected,
getSelfPeerId: () => selfPeerId,
registerPeer
};
})();
+352
View File
@@ -0,0 +1,352 @@
/**
* Shared WebRTC session — signaling via WebHub WS with HTTP fallback.
*/
window.BackOneWebRtcSession = (function () {
let config = {
icePreferOverlay: true,
iceFallbackTimeoutMs: 8000,
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
let pc = null;
let localStream = null;
let remoteStream = null;
let activeCall = null;
let iceFallbackTimer = null;
let httpIcePollTimer = null;
let httpAnswerPollTimer = null;
const listeners = new Set();
function emit(event, detail) {
listeners.forEach(fn => {
try { fn(event, detail); } catch (_) { /* ignore */ }
});
}
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function clearTimers() {
if (iceFallbackTimer) { clearTimeout(iceFallbackTimer); iceFallbackTimer = null; }
if (httpIcePollTimer) { clearInterval(httpIcePollTimer); httpIcePollTimer = null; }
if (httpAnswerPollTimer) { clearInterval(httpAnswerPollTimer); httpAnswerPollTimer = null; }
}
function overlayIceServers() {
return config.icePreferOverlay ? [] : (config.iceServers || []);
}
function fullIceServers() {
return config.iceServers || [];
}
function createPeerConnection(peer) {
clearTimers();
if (pc) {
try { pc.close(); } catch (_) { /* ignore */ }
}
pc = new RTCPeerConnection({ iceServers: overlayIceServers() });
remoteStream = new MediaStream();
pc.ontrack = (ev) => {
ev.streams[0].getTracks().forEach(t => remoteStream.addTrack(t));
emit('remote-track', { stream: remoteStream });
};
pc.onicecandidate = (ev) => {
if (!ev.candidate || !activeCall) return;
relayIce(activeCall.callId, {
candidate: ev.candidate.candidate,
sdpMid: ev.candidate.sdpMid,
sdpMLineIndex: ev.candidate.sdpMLineIndex
});
};
pc.onconnectionstatechange = () => {
emit('connection-state', { state: pc.connectionState });
if (pc.connectionState === 'connected') clearTimers();
};
if (config.icePreferOverlay) {
iceFallbackTimer = setTimeout(() => {
if (!pc || pc.connectionState === 'connected') return;
try {
pc.setConfiguration({ iceServers: fullIceServers() });
pc.restartIce();
} catch (_) { /* ignore */ }
}, config.iceFallbackTimeoutMs || 8000);
}
return pc;
}
async function relayIce(callId, candidate) {
if (BackOneWebHubClient.isConnected()) {
BackOneWebHubClient.send({
type: 'ice_candidate',
callId,
...candidate
});
return;
}
const peerId = BackOneWebHubClient.getSelfPeerId()
|| ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/ice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...candidate, fromPeerId: peerId })
}).catch(() => {});
}
function startHttpIcePoll(callId) {
let since = 0;
httpIcePollTimer = setInterval(async () => {
if (!pc || !activeCall || activeCall.callId !== callId) return;
const peerId = BackOneWebHubClient.getSelfPeerId()
|| ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
try {
const res = await BackOneAuth.fetchWithAuth(
`/controller/comms/call/${encodeURIComponent(callId)}/ice?since=${since}&peerId=${encodeURIComponent(peerId)}`
);
if (!res.ok) return;
const data = await res.json();
const list = Array.isArray(data.candidates) ? data.candidates : [];
for (const c of list) {
try {
await pc.addIceCandidate({
candidate: c.candidate,
sdpMid: c.sdpMid,
sdpMLineIndex: c.sdpMLineIndex
});
since = Math.max(since, c.at || 0);
} catch (_) { /* ignore */ }
}
} catch (_) { /* ignore */ }
}, 1500);
}
function watchHubCallState() {
return BackOneWebHubClient.subscribe('call_state', async (msg) => {
if (!activeCall || msg.callId !== activeCall.callId) return;
if (msg.state === 'accepted' && msg.sdp && activeCall.role === 'caller') {
try {
await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
emit('call-accepted', activeCall);
} catch (_) { /* ignore */ }
}
if (msg.state === 'ended' || msg.state === 'rejected') {
hangUp(false);
}
});
}
let unsubHub = [];
async function waitForOutgoingCallId() {
return new Promise((resolve) => {
const unsub = BackOneWebHubClient.subscribe('call_state', (msg) => {
if (msg.callId && msg.state === 'ringing') {
unsub();
resolve(msg.callId);
}
});
setTimeout(() => { unsub(); resolve(null); }, 5000);
});
}
async function init() {
try {
const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
if (res.ok) {
const cfg = await res.json();
config = { ...config, ...cfg };
}
} catch (_) { /* ignore */ }
unsubHub.forEach(fn => fn && fn());
unsubHub = [
watchHubCallState(),
BackOneWebHubClient.subscribe('ice_candidate', async (msg) => {
if (!pc || !activeCall || msg.callId !== activeCall.callId) return;
try {
await pc.addIceCandidate({
candidate: msg.candidate,
sdpMid: msg.sdpMid,
sdpMLineIndex: msg.sdpMLineIndex
});
} catch (_) { /* ignore */ }
})
];
}
async function getMedia(callType) {
const audio = true;
const video = callType === 'video';
localStream = await navigator.mediaDevices.getUserMedia({ audio, video });
return localStream;
}
function attachLocalTracks() {
if (!pc || !localStream) return;
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
}
async function startOutgoingCall(peer, callType) {
if (!peer || activeCall) throw new Error('Already in a call');
const networkId = BackOneNetworkSession.getNetworkId();
const selfId = ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
createPeerConnection(peer);
localStream = await getMedia(callType);
attachLocalTracks();
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const payload = {
type: 'call_offer',
callType,
networkId,
targetId: peer.peerId,
callerName: peer.displayName || peer.peerId,
sdp: offer.sdp,
overlayIps: peer.ip ? [peer.ip] : []
};
let callId = null;
if (BackOneWebHubClient.isConnected()) {
BackOneWebHubClient.send(payload);
callId = await waitForOutgoingCallId();
if (!callId) throw new Error('Call signaling timeout');
} else {
const res = await BackOneAuth.fetchWithAuth('/controller/comms/call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
callType,
type: callType,
networkId,
targetId: peer.peerId,
callerId: selfId,
callerName: peer.displayName || peer.peerId,
sdp: offer.sdp
})
});
if (!res.ok) throw new Error('Call signaling failed');
const data = await res.json();
callId = data.callId;
startHttpAnswerPoll(callId);
}
activeCall = {
callId,
role: 'caller',
callType,
peer,
networkId
};
if (!BackOneWebHubClient.isConnected()) startHttpIcePoll(activeCall.callId);
emit('call-started', activeCall);
return activeCall;
}
function startHttpAnswerPoll(callId) {
httpAnswerPollTimer = setInterval(async () => {
if (!pc || !activeCall || activeCall.callId !== callId) return;
try {
const res = await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}`);
if (!res.ok) return;
const data = await res.json();
if (data.state === 'accepted' && data.answerSdp) {
await pc.setRemoteDescription({ type: 'answer', sdp: data.answerSdp });
clearInterval(httpAnswerPollTimer);
httpAnswerPollTimer = null;
emit('call-accepted', activeCall);
}
} catch (_) { /* ignore */ }
}, 1500);
}
async function acceptIncomingCall(incoming) {
if (!incoming || activeCall) throw new Error('Already in a call');
const callType = incoming.callType || 'voice';
const callId = incoming.callId;
createPeerConnection({ peerId: incoming.callerId });
localStream = await getMedia(callType);
attachLocalTracks();
await pc.setRemoteDescription({ type: 'offer', sdp: incoming.sdp });
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
if (BackOneWebHubClient.isConnected()) {
BackOneWebHubClient.send({
type: 'call_answer',
callId,
sdp: answer.sdp
});
} else {
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sdp: answer.sdp })
});
startHttpIcePoll(callId);
}
activeCall = {
callId,
role: 'callee',
callType,
peer: { peerId: incoming.callerId, displayName: incoming.callerName },
networkId: incoming.networkId
};
emit('call-started', activeCall);
return activeCall;
}
async function rejectIncomingCall(callId) {
if (BackOneWebHubClient.isConnected()) {
BackOneWebHubClient.send({ type: 'call_reject', callId });
} else {
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/reject`, {
method: 'POST'
}).catch(() => {});
}
}
async function hangUp(signalRemote = true) {
const callId = activeCall?.callId;
clearTimers();
if (signalRemote && callId) {
if (BackOneWebHubClient.isConnected()) {
BackOneWebHubClient.send({ type: 'call_end', callId });
} else {
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/end`, {
method: 'POST'
}).catch(() => {});
}
}
if (pc) {
try { pc.close(); } catch (_) { /* ignore */ }
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(t => t.stop());
localStream = null;
}
remoteStream = null;
activeCall = null;
emit('call-ended', {});
}
return {
init,
subscribe,
startOutgoingCall,
acceptIncomingCall,
rejectIncomingCall,
hangUp,
getPeerConnection: () => pc,
getLocalStream: () => localStream,
getRemoteStream: () => remoteStream,
getActiveCall: () => activeCall,
isInCall: () => !!activeCall
};
})();
+6
View File
@@ -0,0 +1,6 @@
{
"id": "app-tray",
"name": "BackOne Tray",
"version": "1.17.0",
"requires": []
}
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=BackOne Tray
Comment=Start BackOne tray icon at login
Exec=/usr/bin/backone-tray
Icon=network-workgroup
Terminal=false
X-GNOME-Autostart-enabled=true
Hidden=false
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=BackOne Tray
Comment=BackOne system tray for voice, video, and chat
Exec=/usr/bin/backone-tray
Icon=network-workgroup
Terminal=false
Categories=Network;
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""BackOne system tray — opens /app UI and addon deep links."""
import os
import signal
import subprocess
import sys
import threading
import time
import urllib.request
import gi
gi.require_version('Gtk', '3.0')
try:
gi.require_version('AyatanaAppIndicator3', '0.1')
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
except (ImportError, ValueError):
gi.require_version('AppIndicator3', '0.1')
from gi.repository import AppIndicator3
from gi.repository import GLib, Gtk
APP_URL = os.environ.get('BACKONE_APP_URL', 'http://127.0.0.1:9993/app/')
HEALTH_URL = os.environ.get('BACKONE_HEALTH_URL', 'http://127.0.0.1:9993/health')
ICON_NAME = 'network-workgroup'
SERVICE_WAIT_SECONDS = int(os.environ.get('BACKONE_SERVICE_WAIT', '45'))
class BackOneTray:
def __init__(self):
self._windows = []
self._service_ready = False
self.indicator = AppIndicator3.Indicator.new(
'backone-tray',
ICON_NAME,
AppIndicator3.IndicatorCategory.APPLICATION_STATUS,
)
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
self.indicator.set_menu(self._build_menu())
try:
self.indicator.connect('activate', lambda *_: self._open_url(APP_URL))
except TypeError:
pass
self._update_status()
GLib.timeout_add_seconds(30, self._on_timer)
GLib.idle_add(self._bootstrap_service)
def _build_menu(self):
menu = Gtk.Menu()
def add_item(label, callback):
item = Gtk.MenuItem(label=label)
item.connect('activate', lambda *_: callback())
item.show()
menu.append(item)
return item
add_item('Open BackOne App', lambda: self._open_url(APP_URL))
add_item('Voice', lambda: self._open_url(APP_URL + '#/voice'))
add_item('Video', lambda: self._open_url(APP_URL + '#/video'))
add_item('Chat', lambda: self._open_url(APP_URL + '#/chat'))
menu.append(Gtk.SeparatorMenuItem())
add_item('Restart BackOne Service', self._restart_service)
add_item('Quit Tray', Gtk.main_quit)
menu.show()
return menu
def _service_ok(self):
try:
with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp:
return resp.status == 200
except Exception:
return False
def _systemd_active(self):
try:
result = subprocess.run(
['systemctl', 'is-active', 'backone'],
capture_output=True,
text=True,
timeout=5,
)
return result.returncode == 0 and result.stdout.strip() == 'active'
except (OSError, subprocess.TimeoutExpired):
return False
def _start_service(self):
for cmd in (
['systemctl', 'start', 'backone'],
['pkexec', 'systemctl', 'start', 'backone'],
):
try:
result = subprocess.run(
cmd,
capture_output=True,
timeout=30,
)
if result.returncode == 0:
return True
except (OSError, subprocess.TimeoutExpired):
continue
return False
def _wait_for_service(self, timeout=SERVICE_WAIT_SECONDS):
deadline = time.time() + timeout
while time.time() < deadline:
if self._service_ok():
self._service_ready = True
return True
time.sleep(0.5)
return False
def _ensure_service(self):
if self._service_ok():
self._service_ready = True
return True
if not self._systemd_active():
self._start_service()
return self._wait_for_service()
def _bootstrap_service(self):
def worker():
ready = self._ensure_service()
GLib.idle_add(self._on_bootstrap_done, ready)
threading.Thread(target=worker, daemon=True).start()
return False
def _on_bootstrap_done(self, ready):
self._service_ready = ready
self._update_status()
return False
def _update_status(self):
online = self._service_ok()
self._service_ready = online
title = 'BackOne (online)' if online else 'BackOne (offline)'
self.indicator.set_title(title)
def _on_timer(self):
self._update_status()
return True
def _show_offline_dialog(self):
dialog = Gtk.MessageDialog(
transient_for=None,
flags=0,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text='BackOne service is not running',
)
dialog.format_secondary_text(
'Could not reach the BackOne web server on 127.0.0.1:9993.\n\n'
'Use "Restart BackOne Service" from the tray menu, or run:\n'
' sudo systemctl start backone'
)
dialog.run()
dialog.destroy()
def _open_url(self, url):
if not self._ensure_service():
self._show_offline_dialog()
return
if self._try_webkit(url):
return
for cmd in (
['xdg-open', url],
['gio', 'open', url],
['sensible-browser', url],
):
try:
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return
except OSError:
continue
def _try_webkit(self, url):
try:
gi.require_version('WebKit2', '4.1')
except ValueError:
try:
gi.require_version('WebKit2', '4.0')
except ValueError:
return False
from gi.repository import WebKit2
win = Gtk.Window(title='BackOne')
win.set_default_size(900, 640)
view = WebKit2.WebView()
view.load_uri(url)
win.add(view)
win.connect('destroy', lambda w: self._windows.remove(w) if w in self._windows else None)
self._windows.append(win)
win.show_all()
return True
def _restart_service(self):
subprocess.Popen(
['pkexec', 'systemctl', 'restart', 'backone'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def worker():
ready = self._wait_for_service(timeout=30)
GLib.idle_add(self._on_bootstrap_done, ready)
threading.Thread(target=worker, daemon=True).start()
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
BackOneTray()
Gtk.main()
if __name__ == '__main__':
sys.exit(main())
+10 -1
View File
@@ -1,5 +1,5 @@
Name: backone Name: backone
Version: 1.14.1 Version: 1.17.1
Release: 1%{?dist} Release: 1%{?dist}
Summary: BackOne global ethernet switch Summary: BackOne global ethernet switch
@@ -155,6 +155,15 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/backone
%endif %endif
%changelog %changelog
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.17.1
- Fix /api/backone routes and reinstallable Web UI packages
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.17.0
- Tray app, auto web server, peer contact UI
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.16.0
- Release notes
* Tue Oct 8 2024 Dedy Sutanto <dsutanto@backone.cloud> - 1.14.1 * Tue Oct 8 2024 Dedy Sutanto <dsutanto@backone.cloud> - 1.14.1
- see https://github.com/proitlab/BackOne for release notes - see https://github.com/proitlab/BackOne for release notes
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <version> [changelog message]" >&2
exit 1
fi
VERSION="$1"
MESSAGE="${2:-see https://github.com/proitlab/BackOne for release notes}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
IFS='.' read -r MAJ MIN REV <<< "${VERSION%%-*}"
REV="${REV:-0}"
sed -i "s/^#define ZEROTIER_ONE_VERSION_MAJOR .*/#define ZEROTIER_ONE_VERSION_MAJOR ${MAJ}/" "$ROOT/version.h"
sed -i "s/^#define ZEROTIER_ONE_VERSION_MINOR .*/#define ZEROTIER_ONE_VERSION_MINOR ${MIN}/" "$ROOT/version.h"
sed -i "s/^#define ZEROTIER_ONE_VERSION_REVISION .*/#define ZEROTIER_ONE_VERSION_REVISION ${REV}/" "$ROOT/version.h"
sed -i "s/^#define ZEROTIER_ONE_VERSION_BUILD .*/#define ZEROTIER_ONE_VERSION_BUILD 0/" "$ROOT/version.h"
echo "$VERSION" > "$ROOT/debian/version"
"$ROOT/ci/scripts/munge_debian_changelog.sh" "$ROOT/debian/changelog" "$VERSION" \
"Dedy Sutanto <dsutanto@backone.cloud>" "$MESSAGE"
"$ROOT/ci/scripts/munge_rpm_spec.sh" "$ROOT/backone.spec" "$VERSION" \
"Dedy Sutanto <dsutanto@backone.cloud>" "$MESSAGE"
for addon in comms-core webhub voice video chat secure-chat updater app-tray; do
if [ -f "$ROOT/addons/$addon/addon.json" ]; then
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" "$ROOT/addons/$addon/addon.json"
fi
done
echo "Bumped to ${VERSION}"
+1 -1
View File
@@ -31,7 +31,7 @@ else
fi fi
awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \ awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
'BEGIN{print "zerotier-one (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \ 'BEGIN{print "backone (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \
${FILE} > ${FILE}.new ${FILE} > ${FILE}.new
mv ${FILE}.new ${FILE} mv ${FILE}.new ${FILE}
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
set -e
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
SECRET="${BACKONE_HOME}/authtoken.secret"
trim_token() {
tr -d '\n\r \t' < "$1"
}
sync_for_user() {
user="$1"
home="$2"
[ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
user_token="${home}/.backOneOneAuthToken"
if [ -f "$user_token" ] && [ -s "$user_token" ]; then
ut="$(trim_token "$user_token")"
[ -n "$ut" ] || return 0
st=""
if [ -f "$SECRET" ]; then
st="$(trim_token "$SECRET" 2>/dev/null || true)"
fi
if [ "$ut" != "$st" ]; then
printf '%s' "$ut" > "$SECRET"
chown backone:backone "$SECRET" 2>/dev/null || true
chmod 0600 "$SECRET" 2>/dev/null || true
echo "Synced ${SECRET} from ${user_token}"
fi
return 0
fi
if [ -f "$SECRET" ]; then
install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
|| cp "$SECRET" "$user_token"
chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
chmod 0600 "$user_token" 2>/dev/null || true
echo "Installed ${user_token} from ${SECRET}"
fi
}
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
sync_for_user "$SUDO_USER" "$home"
fi
if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
home="$(getent passwd "$USER" | cut -d: -f6)"
sync_for_user "$USER" "$home"
fi
exit 0
+405
View File
@@ -0,0 +1,405 @@
/*
* Copyright (c)2019 ZeroTier, Inc.
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file in the project's root directory.
*/
/****/
#include "CommsController.hpp"
#include "../node/Mutex.hpp"
#include "../node/Utils.hpp"
#include "../osdep/OSUtils.hpp"
#include <map>
#include <set>
#include <sstream>
#include <vector>
namespace ZeroTier {
namespace {
struct IceEntry
{
std::string fromPeerId;
std::string candidate;
std::string sdpMid;
int sdpMLineIndex;
int64_t at;
};
struct CallState
{
std::string callId;
std::string callType;
std::string networkId;
std::string callerId;
std::string calleeId;
std::string callerName;
std::string state;
std::string offerSdp;
std::string answerSdp;
int64_t createdAt;
};
Mutex g_commsMutex;
std::map<std::string, CallState> g_calls;
std::map<std::string, std::vector<IceEntry> > g_iceByCall;
std::map<std::string, std::set<std::string> > g_callsByPeer;
std::string lowerId(const std::string &id)
{
std::string out = id;
for (std::string::iterator i(out.begin()); i != out.end(); ++i) {
if (*i >= 'A' && *i <= 'F') {
*i = (char)(*i - 'A' + 'a');
}
}
return out;
}
std::string newCallId()
{
uint8_t randBytes[8];
Utils::getSecureRandom(randBytes, sizeof(randBytes));
char randHex[17];
Utils::hex(randBytes, (unsigned int)sizeof(randBytes), randHex);
return std::string("call_") + std::to_string(OSUtils::now()) + "_" + randHex;
}
void trackCallForPeer(const std::string &peerId, const std::string &callId)
{
if (peerId.empty()) {
return;
}
g_callsByPeer[peerId].insert(callId);
}
void untrackCall(const CallState &call)
{
const std::string caller = lowerId(call.callerId);
const std::string callee = lowerId(call.calleeId);
if (!caller.empty()) {
g_callsByPeer[caller].erase(call.callId);
if (g_callsByPeer[caller].empty()) {
g_callsByPeer.erase(caller);
}
}
if (!callee.empty()) {
g_callsByPeer[callee].erase(call.callId);
if (g_callsByPeer[callee].empty()) {
g_callsByPeer.erase(callee);
}
}
}
void purgeCall(const std::string &callId)
{
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
if (it != g_calls.end()) {
untrackCall(it->second);
g_calls.erase(it);
}
g_iceByCall.erase(callId);
}
nlohmann::json callToIncomingJson(const CallState &call)
{
nlohmann::json out = nlohmann::json::object();
out["callId"] = call.callId;
out["callType"] = call.callType;
out["networkId"] = call.networkId;
out["callerId"] = call.callerId;
out["callerName"] = call.callerName;
out["sdp"] = call.offerSdp;
out["state"] = call.state;
return out;
}
nlohmann::json webrtcConfigFromLocal(const nlohmann::json &localConfig)
{
nlohmann::json out = nlohmann::json::object();
out["icePreferOverlay"] = true;
out["iceFallbackTimeoutMs"] = 8000;
out["webhubPort"] = 9994;
out["iceServers"] = nlohmann::json::array();
out["iceServers"].push_back(nlohmann::json::object({ { "urls", "stun:stun.l.google.com:19302" } }));
try {
if (localConfig.contains("settings") && localConfig["settings"].is_object()) {
const nlohmann::json &settings = localConfig["settings"];
if (settings.contains("icePreferOverlay") && settings["icePreferOverlay"].is_boolean()) {
out["icePreferOverlay"] = settings["icePreferOverlay"].get<bool>();
}
if (settings.contains("iceFallbackTimeoutMs") && settings["iceFallbackTimeoutMs"].is_number_integer()) {
out["iceFallbackTimeoutMs"] = settings["iceFallbackTimeoutMs"].get<int64_t>();
}
if (settings.contains("webhubPort") && settings["webhubPort"].is_number_integer()) {
out["webhubPort"] = settings["webhubPort"].get<int64_t>();
}
if (settings.contains("iceServers") && settings["iceServers"].is_array()) {
out["iceServers"] = settings["iceServers"];
}
}
} catch ( ... ) {
}
return out;
}
} // namespace
void CommsController::configureRoutes(
httplib::Server &s,
httplib::Server &sv6,
const std::function<nlohmann::json()> &localConfigGetter,
const std::function<void(const httplib::Request&, httplib::Response&, std::string)> &setContent)
{
auto webrtcConfigGet = [localConfigGetter, setContent](const httplib::Request &req, httplib::Response &res) {
nlohmann::json localConfig = nlohmann::json::object();
try {
localConfig = localConfigGetter();
} catch ( ... ) {
}
setContent(req, res, webrtcConfigFromLocal(localConfig).dump());
};
s.Get("/app/api/webrtc-config", webrtcConfigGet);
sv6.Get("/app/api/webrtc-config", webrtcConfigGet);
auto callCreate = [setContent](const httplib::Request &req, httplib::Response &res) {
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
if (!body.is_object()) {
res.status = 400;
setContent(req, res, "{\"error\":\"body must be a JSON object\"}");
return;
}
const std::string targetId = lowerId(body.contains("targetId") && body["targetId"].is_string()
? body["targetId"].get<std::string>() : "");
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
? body["sdp"].get<std::string>() : "";
if (targetId.empty() || sdp.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"targetId and sdp are required\"}");
return;
}
std::string callType = "voice";
if (body.contains("callType") && body["callType"].is_string()) {
callType = body["callType"].get<std::string>();
} else if (body.contains("type") && body["type"].is_string()) {
const std::string t = body["type"].get<std::string>();
if (t == "voice" || t == "video") {
callType = t;
}
}
const std::string networkId = body.contains("networkId") && body["networkId"].is_string()
? lowerId(body["networkId"].get<std::string>()) : "";
const std::string callerId = body.contains("callerId") && body["callerId"].is_string()
? lowerId(body["callerId"].get<std::string>()) : "local";
const std::string callerName = body.contains("callerName") && body["callerName"].is_string()
? body["callerName"].get<std::string>() : callerId;
CallState call;
call.callId = newCallId();
call.callType = callType;
call.networkId = networkId;
call.callerId = callerId;
call.calleeId = targetId;
call.callerName = callerName;
call.state = "ringing";
call.offerSdp = sdp;
call.createdAt = OSUtils::now();
Mutex::Lock _l(g_commsMutex);
g_calls[call.callId] = call;
trackCallForPeer(call.callerId, call.callId);
trackCallForPeer(call.calleeId, call.callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = call.callId;
out["state"] = call.state;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call", callCreate);
sv6.Post("/controller/comms/call", callCreate);
auto callIncoming = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
if (peerId.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"peerId query parameter is required\"}");
return;
}
nlohmann::json calls = nlohmann::json::array();
Mutex::Lock _l(g_commsMutex);
std::map<std::string, std::set<std::string> >::const_iterator it = g_callsByPeer.find(peerId);
if (it != g_callsByPeer.end()) {
for (std::set<std::string>::const_iterator c = it->second.begin(); c != it->second.end(); ++c) {
std::map<std::string, CallState>::const_iterator callIt = g_calls.find(*c);
if (callIt == g_calls.end()) {
continue;
}
if (lowerId(callIt->second.calleeId) != peerId || callIt->second.state != "ringing") {
continue;
}
calls.push_back(callToIncomingJson(callIt->second));
}
}
nlohmann::json out = nlohmann::json::object();
out["calls"] = calls;
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/incoming", callIncoming);
sv6.Get("/controller/comms/call/incoming", callIncoming);
auto callAnswer = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
? body["sdp"].get<std::string>() : "";
if (sdp.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"sdp is required\"}");
return;
}
Mutex::Lock _l(g_commsMutex);
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
if (it == g_calls.end()) {
res.status = 404;
setContent(req, res, "{\"error\":\"call not found\"}");
return;
}
it->second.answerSdp = sdp;
it->second.state = "accepted";
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "accepted";
out["sdp"] = sdp;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
sv6.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
auto callReject = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
purgeCall(callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "rejected";
setContent(req, res, out.dump());
};
s.Post("/controller/comms/call/([^/]+)/reject", callReject);
sv6.Post("/controller/comms/call/([^/]+)/reject", callReject);
auto callEnd = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
purgeCall(callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "ended";
setContent(req, res, out.dump());
};
s.Post("/controller/comms/call/([^/]+)/end", callEnd);
sv6.Post("/controller/comms/call/([^/]+)/end", callEnd);
auto callIcePost = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
IceEntry entry;
entry.fromPeerId = body.contains("fromPeerId") && body["fromPeerId"].is_string()
? lowerId(body["fromPeerId"].get<std::string>()) : "";
entry.candidate = body.contains("candidate") && body["candidate"].is_string()
? body["candidate"].get<std::string>() : "";
entry.sdpMid = body.contains("sdpMid") && body["sdpMid"].is_string()
? body["sdpMid"].get<std::string>() : "";
entry.sdpMLineIndex = body.contains("sdpMLineIndex") && body["sdpMLineIndex"].is_number_integer()
? (int)body["sdpMLineIndex"].get<int64_t>() : 0;
entry.at = OSUtils::now();
if (entry.candidate.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"candidate is required\"}");
return;
}
Mutex::Lock _l(g_commsMutex);
g_iceByCall[callId].push_back(entry);
nlohmann::json out = nlohmann::json::object();
out["ok"] = true;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
sv6.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
auto callIceGet = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
int64_t since = 0;
if (req.has_param("since")) {
try {
since = std::stoll(req.get_param_value("since"));
} catch ( ... ) {
since = 0;
}
}
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
nlohmann::json candidates = nlohmann::json::array();
Mutex::Lock _l(g_commsMutex);
std::map<std::string, std::vector<IceEntry> >::const_iterator it = g_iceByCall.find(callId);
if (it != g_iceByCall.end()) {
for (std::vector<IceEntry>::const_iterator e = it->second.begin(); e != it->second.end(); ++e) {
if (e->at <= since) {
continue;
}
if (!peerId.empty() && e->fromPeerId == peerId) {
continue;
}
nlohmann::json item = nlohmann::json::object();
item["candidate"] = e->candidate;
item["sdpMid"] = e->sdpMid;
item["sdpMLineIndex"] = e->sdpMLineIndex;
item["at"] = e->at;
candidates.push_back(item);
}
}
nlohmann::json out = nlohmann::json::object();
out["candidates"] = candidates;
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
sv6.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
auto callGet = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
std::map<std::string, CallState>::const_iterator it = g_calls.find(callId);
if (it == g_calls.end()) {
res.status = 404;
setContent(req, res, "{\"error\":\"call not found\"}");
return;
}
nlohmann::json out = callToIncomingJson(it->second);
if (it->second.state == "accepted" && !it->second.answerSdp.empty()) {
out["answerSdp"] = it->second.answerSdp;
}
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/([^/]+)", callGet);
sv6.Get("/controller/comms/call/([^/]+)", callGet);
}
} // namespace ZeroTier
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (c)2019 ZeroTier, Inc.
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file in the project's root directory.
*/
/****/
#ifndef ZT_COMMSCONTROLLER_HPP
#define ZT_COMMSCONTROLLER_HPP
#include <functional>
#include <string>
#include <cpp-httplib/httplib.h>
#include <nlohmann/json.hpp>
namespace ZeroTier {
/**
* Ephemeral in-memory WebRTC signaling fallback and webrtc-config API.
*/
class CommsController
{
public:
void configureRoutes(
httplib::Server &s,
httplib::Server &sv6,
const std::function<nlohmann::json()> &localConfigGetter,
const std::function<void(const httplib::Request&, httplib::Response&, std::string)> &setContent);
};
} // namespace ZeroTier
#endif
+6
View File
@@ -1120,6 +1120,12 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
}; };
s.Get(memberListPath2, memberListGet2); s.Get(memberListPath2, memberListGet2);
sv6.Get(memberListPath2, memberListGet2); sv6.Get(memberListPath2, memberListGet2);
std::string appMemberListPath = "/app/api/network/([0-9a-fA-F]{16})/members";
s.Get(appMemberListPath, memberListGet2);
sv6.Get(appMemberListPath, memberListGet2);
std::string backoneMemberListPath = "/api/backone/network/([0-9a-fA-F]{16})/members";
s.Get(backoneMemberListPath, memberListGet2);
sv6.Get(backoneMemberListPath, memberListGet2);
auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) { auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
auto networkID = req.matches[1]; auto networkID = req.matches[1];
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27 ext/libpqxx-7.7.3/config/config.guess
1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f ext/libpqxx-7.7.3/config/config.sub
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-addon-chat`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-addon-comms-core`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-addon-updater`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-addon-video`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-addon-voice`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-app-shell`.
Whitespace-only changes.
@@ -0,0 +1,257 @@
backone (1.17.1) stable; urgency=medium
* Fix /api/backone routes and reinstallable Web UI packages
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
backone (1.17.0) stable; urgency=medium
* Tray app, auto web server, peer contact UI
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
backone (1.16.0) stable; urgency=medium
* Release notes
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
backone (1.15.0) stable; urgency=medium
* Separated addon architecture: voice, video, chat, updater
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
zerotier-one (1.14.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
zerotier-one (1.14.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
zerotier-one (1.12.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
zerotier-one (1.12.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
zerotier-one (1.12.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
zerotier-one (1.10.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
zerotier-one (1.10.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
zerotier-one (1.10.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
zerotier-one (1.10.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
zerotier-one (1.10.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
zerotier-one (1.10.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
zerotier-one (1.10.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
zerotier-one (1.8.10) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
zerotier-one (1.8.9) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
zerotier-one (1.8.8) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
zerotier-one (1.8.7) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
zerotier-one (1.8.6) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
zerotier-one (1.8.5) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
zerotier-one (1.8.4) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
zerotier-one (1.8.3) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
zerotier-one (1.8.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
zerotier-one (1.8.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
zerotier-one (1.8.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
zerotier-one (1.6.6) unstable; urgency=medium
* Backport endpoint mitigation against address collision attack.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
zerotier-one (1.6.5) unstable; urgency=medium
* Fix path filtering bug that could cause "software laser" effect.
* Fix printf overflow in CLI (not exploitable or security related)
* Fix Windows device enumeration issue.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
zerotier-one (1.6.4) unstable; urgency=medium
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
zerotier-one (1.6.3-1) unstable; urgency=medium
* Fix a problem causing nodes to go into a "coma" with some network configurations.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
zerotier-one (1.6.2-2) unstable; urgency=medium
* This is a minor update to the 1.6.2 package to address issues with
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
zerotier-one (1.6.2) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
zerotier-one (1.6.1) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
zerotier-one (1.6.0) unstable; urgency=medium
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
zerotier-one (1.5.0) unstable; urgency=medium
* Version 1.5.0 is actually 1.6.0-beta1
* See RELEASE-NOTES.md for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
zerotier-one (1.4.6) unstable; urgency=medium
* Update default root server list
* Fix build flags on "armhf" (32-bit ARM) platforms for better
compatibility with Pi Zero and other devices.
* Fix license text in one.cpp.
* Add a clarification to LICENSE.txt.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
zerotier-one (1.4.4) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* License changed to BSL 1.1
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2-2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
* This is a new build that fixes a binary build issue with containers and SELinux
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.2) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
zerotier-one (1.4.0) unstable; urgency=medium
* See https://github.com/zerotier/ZeroTierOne for release notes.
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
# Older entries have been removed from this changelog.
# To read the complete changelog use `apt changelog backone-app-tray`.
Whitespace-only changes.
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
#DEBHELPER#
exit 0
+2
View File
@@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=
+12
View File
@@ -0,0 +1,12 @@
Package: backone-addon-chat
Source: backone
Version: 1.17.1
Architecture: all
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
Installed-Size: 28
Depends: backone, backone-addon-comms-core, backone-app-shell
Section: net
Priority: optional
Homepage: https://backone.cloud
Description: BackOne chat P2P addon
Text chat with image and document attachments over BackOne.
+3
View File
@@ -0,0 +1,3 @@
cb73fba9ab4a23ff5f4dc1e3fa7d6520 usr/lib/backone/addons/chat/addon.json
6020963c701ef2f1c4c4c8a78f2d6845 usr/share/doc/backone-addon-chat/changelog.gz
463936e9518d72d5ce8f85b746e0b8b5 var/lib/backone/app/addons/chat/index.js
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
exit 0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
/usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
#DEBHELPER#
exit 0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
#DEBHELPER#
exit 0
+2
View File
@@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=
+13
View File
@@ -0,0 +1,13 @@
Package: backone-addon-comms-core
Source: backone
Version: 1.17.1
Architecture: amd64
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
Installed-Size: 15
Depends: backone
Recommends: postgresql-client, redis-tools
Section: net
Priority: optional
Homepage: https://backone.cloud
Description: BackOne comms core addon (signaling backend)
Shared signaling backend for voice, video, and chat addons.
+3
View File
@@ -0,0 +1,3 @@
afd892e714b0637552f6fc56040e860a usr/lib/backone/addons/comms-core/addon.json
51ec6efb47fa207fa716611711defd8f usr/lib/backone/addons/comms-core/schema.sql
4b992da672501c1ed9da4a0c09d59986 usr/share/doc/backone-addon-comms-core/changelog.gz
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
exit 0
+12
View File
@@ -0,0 +1,12 @@
Package: backone-addon-secure-chat
Source: backone
Version: 1.17.1
Architecture: all
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
Installed-Size: 24
Depends: backone, backone-addon-comms-core, backone-app-shell
Section: net
Priority: optional
Homepage: https://backone.cloud
Description: BackOne secure chat addon
Text-only secure chat over BackOne with a dedicated authenticated API.
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
/usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
exit 0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
#DEBHELPER#
exit 0
+2
View File
@@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=
+12
View File
@@ -0,0 +1,12 @@
Package: backone-addon-updater
Source: backone
Version: 1.17.1
Architecture: all
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
Installed-Size: 28
Depends: backone
Section: net
Priority: optional
Homepage: https://backone.cloud
Description: BackOne version updater addon
Migrates installations between BackOne and addon versions.
+8
View File
@@ -0,0 +1,8 @@
bee18cfe55163e6f4cf1d8a22c19dfa8 usr/lib/backone/addons/updater/addon.json
fbc9822a89457454f09b87ba35182052 usr/lib/backone/addons/updater/backone-updater
86727de17602883708f38bdc69d68196 usr/lib/backone/addons/updater/merge-local-conf.sh
1a944e1b0c1afdcff4b75c11f1a18ce0 usr/lib/backone/addons/updater/migrations/001_zerotier_to_backone.sh
5f8f8cd7bba7204f08b4d1a7e7b0dd4c usr/lib/backone/addons/updater/migrations/002_register_addon_host.sh
2fac4021a24721dee1bac76557171998 usr/lib/backone/addons/updater/migrations/003_enable_webserver_defaults.sh
9e532d47a1b291f26457aaff04864920 usr/lib/backone/addons/updater/rebuild-addon-manifest.sh
dbf8511a07dbd23850fe2553d78dcf6a usr/share/doc/backone-addon-updater/changelog.gz
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
exit 0
+1
View File
@@ -0,0 +1 @@
../lib/backone/addons/updater/backone-updater
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh -e
case "$1" in
configure)
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
fi
;;
esac
#DEBHELPER#
exit 0
+2
View File
@@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=
+12
View File
@@ -0,0 +1,12 @@
Package: backone-addon-video
Source: backone
Version: 1.17.1
Architecture: all
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
Installed-Size: 25
Depends: backone, backone-addon-comms-core, backone-addon-voice, backone-app-shell
Section: net
Priority: optional
Homepage: https://backone.cloud
Description: BackOne video call P2P addon
P2P video calling with ringing popup over the BackOne overlay.
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more