upstream: integrar Oasis 1.3.3
Diferencia 1.3.1 -> 1.3.3 del codigo de la APK oficial aplicada a tres bandas encima de la rama: karma en el perfil, salas con directo, fotos selladas, visibilidad de menciones, telefono por stream en Android y las traducciones nuevas. Sin dependencias nuevas. OasisMobile.css sigue siendo el del fork; del oficial solo se traen al final las reglas de las piezas nuevas (karma, directo a pantalla completa, salas en vivo, graphos).
This commit is contained in:
parent
bc64565e9c
commit
948b6460d9
95 changed files with 3613 additions and 1356 deletions
|
|
@ -1,6 +1,6 @@
|
|||
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false }) => {
|
||||
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false, filter = 'status', advanced = false, health = null, lan = null }) => {
|
||||
const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderStateChip } = require('./main_views');
|
||||
const { template, i18n, renderStateChip, renderModuleStats } = require('./main_views');
|
||||
const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health');
|
||||
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
||||
const isOutdated = (v) => {
|
||||
|
|
@ -10,6 +10,8 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
};
|
||||
const stale = new Set(staleKeys || []);
|
||||
const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k));
|
||||
const knowsVersion = (keys) => keys.some(k => (versions || {})[k] || stale.has(k));
|
||||
const unspecifiedHost = (h) => /^(0\.0\.0\.0|::|\[::\])$/.test(String(h || ''));
|
||||
const versionCell = (k) => {
|
||||
const v = (versions || {})[k];
|
||||
if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'];
|
||||
|
|
@ -56,7 +58,7 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
};
|
||||
const onion = new Set(onionKeys || []);
|
||||
const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key));
|
||||
const renderPeerRow = (peerData) => {
|
||||
const renderPeerRow = (peerData, showVersion) => {
|
||||
const peer = peerData[1];
|
||||
const { name, users, key } = peer;
|
||||
const tor = overTor(peerData);
|
||||
|
|
@ -64,9 +66,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
const filteredUsers = (users || []).filter(u => u.id !== key);
|
||||
const userCount = Array.isArray(users) ? filteredUsers.length : null;
|
||||
return tr(
|
||||
td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
||||
td({ 'data-label': i18n.peerHost || 'Pub' }, unspecifiedHost(name) ? '—' : a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
||||
td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)),
|
||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)),
|
||||
showVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)) : null,
|
||||
td({ 'data-label': i18n.peersTorColumn }, tor
|
||||
? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓')
|
||||
: span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')),
|
||||
|
|
@ -84,18 +86,20 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
|
||||
const renderPeerTable = (peers, emptyKey) => {
|
||||
if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections);
|
||||
const showVersion = knowsVersion(peers.map(pd => pd[1].key));
|
||||
return table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peerHost || 'Pub'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
||||
showVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, i18n.peersTorColumn),
|
||||
td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated')
|
||||
),
|
||||
...peers.map(renderPeerRow)
|
||||
...peers.map(pd => renderPeerRow(pd, showVersion))
|
||||
);
|
||||
};
|
||||
|
||||
const techVersion = knowsVersion((technicalPeers || []).map(tp => tp.key));
|
||||
const technicalRows = (technicalPeers || []).map(tp => {
|
||||
const k = tp.key || '';
|
||||
const connected = tp.state === 'connected';
|
||||
|
|
@ -105,9 +109,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')),
|
||||
onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))
|
||||
? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||
: td({ 'data-label': i18n.peerAddressLabel, title: String(tp.host || ''), class: 'peer-host' }, tp.host ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
||||
: td({ 'data-label': i18n.peerAddressLabel, title: unspecifiedHost(tp.host) ? '' : String(tp.host || ''), class: 'peer-host' }, tp.host && !unspecifiedHost(tp.host) ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
||||
td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')),
|
||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)),
|
||||
techVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)) : null,
|
||||
td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)),
|
||||
td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')),
|
||||
td(
|
||||
|
|
@ -120,31 +124,44 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
)
|
||||
);
|
||||
});
|
||||
const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh'));
|
||||
const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle'));
|
||||
const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'));
|
||||
const withCount = (label, n) => Number(n) > 0 ? `${label} (${n})` : label;
|
||||
const peersToolbar = (tabName, total, segments) => {
|
||||
if (!(Number(total) > 0)) return null;
|
||||
const back = input({ type: "hidden", name: "returnTo", value: `/peers?filter=${tabName}` });
|
||||
return div({ class: 'filters activity-filter-chips activity-toolbar-row' },
|
||||
renderModuleStats(total, segments),
|
||||
div({ class: "conn-actions peers-conn-actions filter-box" },
|
||||
form({ action: "/peers/refresh", method: "post" }, back, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh')),
|
||||
form({ action: "/peers/prune", method: "post" }, back, tabName === 'lan' ? input({ type: "hidden", name: "scope", value: "lan" }) : null, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle')),
|
||||
form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'))
|
||||
)
|
||||
);
|
||||
};
|
||||
const importForm = form(
|
||||
{ action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" },
|
||||
label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'),
|
||||
br(),
|
||||
textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }),
|
||||
br(),
|
||||
input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }),
|
||||
br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import')
|
||||
);
|
||||
|
||||
const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' },
|
||||
h2(i18n.peerConnectionsTitle || 'Connections'),
|
||||
const peersTechnicalBlock = div({ class: 'peers-technical-block' },
|
||||
peersToolbar('connections', technicalPeers.length, [
|
||||
{ label: i18n.peerStateConnected, count: technicalPeers.filter(tp => tp.state === 'connected').length },
|
||||
{ label: i18n.peerOutdated, count: technicalPeers.filter(tp => outdatedKey(tp.key)).length },
|
||||
{ label: 'TOR', count: technicalPeers.filter(tp => onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peerConnectionsTitle || 'Connections', technicalPeers.length)), p(i18n.peersConnectionsHint)),
|
||||
div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)),
|
||||
div({ class: "conn-actions peers-conn-actions filters" }, refreshButton, pruneButton, exportButton),
|
||||
technicalPeers.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peersOasisId),
|
||||
td({ class: 'card-label' }, i18n.peerAddressLabel),
|
||||
td({ class: 'card-label peer-port' }, i18n.peerPort),
|
||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
||||
techVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peerLastChangeLabel),
|
||||
td({ class: 'card-label' }, '')
|
||||
|
|
@ -155,6 +172,121 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
importForm
|
||||
);
|
||||
|
||||
const fmtDate = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—';
|
||||
const healthRows = health && Array.isArray(health.rows) ? health.rows : [];
|
||||
const healthKind = (r) => {
|
||||
if (r.state === 'connected') return 'connected';
|
||||
if (r.dead) return 'unreachable';
|
||||
if ((Number(r.failures) || 0) > 0 || r.reason) return 'failing';
|
||||
return 'reachable';
|
||||
};
|
||||
const healthChip = (r) => {
|
||||
const k = healthKind(r);
|
||||
if (k === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase());
|
||||
if (k === 'unreachable') return renderStateChip('closed', null, String(i18n.graphosFedUnreachable).toUpperCase());
|
||||
if (k === 'failing') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[r.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase());
|
||||
return renderStateChip(outdatedKey(r.key) ? 'whole' : 'mutuals', null, String(i18n.peersHealthReachable).toUpperCase());
|
||||
};
|
||||
const healthCount = (k) => healthRows.filter(r => healthKind(r) === k).length;
|
||||
const deadCount = healthRows.filter(r => r.dead).length;
|
||||
const healthVersion = knowsVersion(healthRows.map(r => r.key));
|
||||
const healthBlock = !healthRows.length ? div({ class: 'peers-list' }, p(i18n.noConnections)) : div({ class: 'peers-technical-block peers-health-block' },
|
||||
peersToolbar('health', healthRows.length, [
|
||||
{ label: i18n.peersHealthReachable, count: healthCount('connected') + healthCount('reachable') },
|
||||
{ label: i18n.peersHealthFailing, count: healthCount('failing') },
|
||||
{ label: i18n.graphosFedUnreachable, count: deadCount },
|
||||
{ label: i18n.peerOutdated, count: healthRows.filter(r => outdatedKey(r.key)).length },
|
||||
{ label: 'TOR', count: healthRows.filter(r => r.tor).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peersTabHealth, healthRows.length)), p(i18n.peersHealthHint)),
|
||||
deadCount
|
||||
? div({ class: "conn-actions peers-conn-actions filters" },
|
||||
form({ action: "/peers/clear-unreachable", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peersHealthForget)))
|
||||
: null,
|
||||
healthRows.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peerHost || 'IP'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||
td({ class: 'card-label' }, i18n.graphosFedTransport),
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peersHealthFailures),
|
||||
td({ class: 'card-label' }, i18n.peersFailingSince),
|
||||
healthVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null
|
||||
),
|
||||
...healthRows.map(r => tr(
|
||||
r.tor
|
||||
? td({ 'data-label': i18n.peerHost || 'IP', class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||
: td({ 'data-label': i18n.peerHost || 'IP', title: unspecifiedHost(r.host) ? '' : String(r.host || ''), class: 'peer-host' }, r.host && !unspecifiedHost(r.host) ? shortHost(r.host) : '—'),
|
||||
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||
td({ 'data-label': i18n.graphosFedTransport }, r.tor ? renderStateChip('fediverse', null, 'TOR') : renderStateChip('transport', null, String(i18n.graphosFedClearnet).toUpperCase())),
|
||||
td({ 'data-label': i18n.peerStateLabel }, healthChip(r)),
|
||||
td({ 'data-label': i18n.peersHealthFailures }, String(Number(r.failures) || 0)),
|
||||
td({ 'data-label': i18n.peersFailingSince }, r.failingSince ? fmtDate(r.failingSince) : '—'),
|
||||
healthVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null
|
||||
))
|
||||
)
|
||||
: p(i18n.noConnections)
|
||||
);
|
||||
|
||||
const lanRows = lan && Array.isArray(lan.rows) ? lan.rows : [];
|
||||
const lanOn = !!(lan && lan.broadcasting);
|
||||
const lanVersion = knowsVersion(lanRows.map(r => r.key));
|
||||
const lanBlock = div({ class: 'peers-technical-block peers-lan-block' },
|
||||
peersToolbar('lan', lanRows.length, [
|
||||
{ label: i18n.online, count: lanRows.filter(r => r.online).length },
|
||||
{ label: i18n.peerOutdated, count: lanRows.filter(r => outdatedKey(r.key)).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peersTabLan, lanRows.length)), p(i18n.peersLanHint)),
|
||||
div({ class: "peers-pause-row" },
|
||||
p({ class: "peers-pause-hint" }, `${i18n.settingsLanTitle}: `, renderStateChip(lanOn ? 'mutuals' : 'hidden', null, String(lanOn ? i18n.modulesEnabledModulesLabel : i18n.modulesDisabledModulesLabel).toUpperCase()))
|
||||
),
|
||||
lanRows.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peersLanLastSeen),
|
||||
lanVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, '')
|
||||
),
|
||||
...lanRows.map(r => tr(
|
||||
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||
td({ 'data-label': i18n.peerStateLabel }, r.online ? renderStateChip('mutuals', null, String(i18n.online).toUpperCase()) : renderStateChip('hidden', null, String(i18n.offline).toUpperCase())),
|
||||
td({ 'data-label': i18n.peersLanLastSeen }, fmtDate(r.seenAt)),
|
||||
lanVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null,
|
||||
td(r.online ? null : form({ method: "POST", action: "/peers/lan/forget", class: "inline-form" },
|
||||
input({ type: "hidden", name: "key", value: r.key }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.delete)
|
||||
))
|
||||
))
|
||||
)
|
||||
: p(i18n.peersLanEmpty)
|
||||
);
|
||||
|
||||
const tabs = ['status', 'connections'].concat(advanced ? ['lan', 'health'] : []);
|
||||
const tab = tabs.includes(filter) ? filter : 'status';
|
||||
const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth };
|
||||
const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [
|
||||
{ label: i18n.online, count: onlineCount },
|
||||
{ label: i18n.discovered, count: discoveredCount },
|
||||
{ label: i18n.unknown, count: unknownCount }
|
||||
]);
|
||||
const statusBlock = (onlineCount + discoveredCount + unknownCount) > 0
|
||||
? div({ class: "peers-list" },
|
||||
statusToolbar,
|
||||
...[
|
||||
[i18n.online, onlineCount, dedupOnline, 'noConnections', i18n.peersOnlineHint],
|
||||
[i18n.discovered, discoveredCount, dedupDiscovered, 'noDiscovered', i18n.peersDiscoveredHint],
|
||||
[i18n.unknown, unknownCount, dedupUnknown, 'noUnknownPeers', i18n.peersUnknownHint]
|
||||
].filter(([, count]) => count > 0).flatMap(([title, count, list, emptyKey, hint], idx) => [
|
||||
idx ? hr() : null,
|
||||
div({ class: "tags-header" }, h2(withCount(title, count)), p(hint)),
|
||||
renderPeerTable(list, emptyKey)
|
||||
])
|
||||
)
|
||||
: div({ class: "peers-list" }, statusToolbar, p(i18n.noConnections));
|
||||
|
||||
return template(
|
||||
i18n.peers,
|
||||
section(
|
||||
|
|
@ -162,20 +294,14 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
h2(i18n.peers),
|
||||
p(i18n.peerConnectionsIntro)
|
||||
),
|
||||
(onlineCount + discoveredCount + unknownCount) > 0
|
||||
? div({ class: "peers-list" },
|
||||
div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)),
|
||||
renderPeerTable(dedupOnline, 'noConnections'),
|
||||
hr(),
|
||||
div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)),
|
||||
renderPeerTable(dedupDiscovered, 'noDiscovered'),
|
||||
hr(),
|
||||
div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)),
|
||||
renderPeerTable(dedupUnknown, 'noUnknownPeers')
|
||||
)
|
||||
: null,
|
||||
peersTechnicalBlock,
|
||||
p(i18n.connectionActionIntro)
|
||||
div({ class: 'mode-buttons-row' },
|
||||
...tabs.map(t => a({ class: tab === t ? 'filter-btn active' : 'filter-btn', href: `/peers?filter=${t}` }, String(tabLabel[t]).toUpperCase()))
|
||||
),
|
||||
tab === 'status' ? statusBlock : null,
|
||||
tab === 'connections' ? peersTechnicalBlock : null,
|
||||
tab === 'connections' ? p(i18n.connectionActionIntro) : null,
|
||||
tab === 'lan' ? lanBlock : null,
|
||||
tab === 'health' ? healthBlock : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue