channels: present authenticated hub profiles

This commit is contained in:
DeFiDude 2026-07-31 05:15:24 -06:00
parent 5bdaeb6330
commit bb40092210
7 changed files with 832 additions and 29 deletions

View file

@ -132,6 +132,9 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
let hub_switcher_test =
read_source(root.join("dashboard/scripts/test_channels_hub_switcher.js"))
.expect("channels hub switcher test");
let hub_profile_test =
read_source(root.join("dashboard/scripts/test_channels_hub_profile.js"))
.expect("channels hub profile test");
let tauri_events =
read_source(root.join("dashboard/static/js/tauri_events.js")).expect("tauri event bridge");
let tauri_lib = read_source(root.join("src-tauri/src/lib.rs")).expect("tauri lib");
@ -244,6 +247,8 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
assert!(channels_css.contains(".channels-layout:not(.room-live)"));
assert!(channels_css.contains(".channel-transition-rail"));
assert!(channels_css.contains(".channel-hub-greeting"));
assert!(channels_css.contains(".channel-hub-profile-capabilities"));
assert!(channels_css.contains(".channel-hub-greeting-delivery"));
assert!(channels_css.contains(".channel-presence-summary"));
assert!(channels_css.contains(".channel-history-rail"));
assert!(channels_css.contains(".channel-day-separator"));
@ -431,6 +436,7 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
assert!(channels_css.contains(".channel-share-input"));
assert!(share_test.contains("channel share tests passed"));
assert!(hub_switcher_test.contains("channel hub switcher tests passed"));
assert!(hub_profile_test.contains("channel hub profile tests passed"));
}
#[test]

View file

@ -277,7 +277,7 @@ async function main() {
assert(navSource.indexOf('channelsRefreshDirectory(false);') !== -1,
'entering the Channels view must request an idle or stale directory');
assert(channelsSource.indexOf('service_model_version: 2') !== -1);
assert(channelsSource.indexOf('service_model_version: 3') !== -1);
assert(channelsSource.indexOf('channelsOpenJoinSheet(room.name)') !== -1);
assert(channelsSource.indexOf("RS.invoke('refresh_channel_directory')") !== -1);

View file

@ -0,0 +1,200 @@
#!/usr/bin/env node
// Deterministic typed-model and source coverage for authenticated hub identity,
// welcome provenance, capabilities, limits, and public-directory presentation.
'use strict';
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var vm = require('vm');
var root = path.join(__dirname, '..');
var channelsSource = fs.readFileSync(
path.join(root, 'static', 'js', 'channels.js'),
'utf8'
);
var hubSource = fs.readFileSync(
path.join(root, 'static', 'js', 'channel_hub.js'),
'utf8'
);
var cssSource = fs.readFileSync(
path.join(root, 'static', 'css', '09-channels.css'),
'utf8'
);
var responsiveSource = fs.readFileSync(
path.join(root, 'static', 'css', '13-responsive.css'),
'utf8'
);
function sourceRange(startName, endName) {
var start = channelsSource.indexOf('function ' + startName);
var end = channelsSource.indexOf('\nfunction ' + endName, start);
assert(start !== -1 && end !== -1, startName + ' source range must exist');
return channelsSource.slice(start, end);
}
var modelSource = sourceRange(
'_channelsCurrentHubObserved',
'_channelsConnectCommandBlocked'
);
var context = {
Array: Array,
Math: Math,
Number: Number,
String: String,
channelsSnapshot: {}
};
vm.runInNewContext(modelSource, context, {
filename: 'channels-hub-profile-model.js'
});
var HUB = '00112233445566778899aabbccddeeff';
var IDENTITY = 'ffeeddccbbaa99887766554433221100';
var greeting = {
text: 'Read the field rules before transmitting.',
received_at_ms: 1_900_000_000_000,
source_hash: IDENTITY,
delivery: 'resource',
completeness: 'complete'
};
context.channelsSnapshot = {
protocol_version: '0.1.3',
phase: 'active',
nickname: 'Field Rat',
selected_hub_destination: HUB,
hub: {
destination_hash: HUB,
name: 'Legacy root projection'
},
directory: { phase: 'idle', rooms: [] },
hub_greeting: null,
hubs: [{
destination_hash: HUB,
observed: {
phase: 'active',
nickname: 'Field Rat',
hub: {
destination_hash: HUB,
identity_hash: IDENTITY,
announced_name: 'Ridge Relay',
name: 'Ridge Operations',
version: '0.3.2',
hops: 2,
link_mdu: 383,
connected_at_ms: 1_900_000_000_000,
capabilities: {
actions: true,
direct_notices: true,
resource_envelopes: true,
rejoin_grace: false
},
limits: {
max_nick_bytes: 32,
max_room_name_bytes: 64,
max_message_body_bytes: 350,
max_rooms_per_session: 32,
rate_messages_per_minute: 240
}
},
directory: {
phase: 'ready',
rooms: [{ name: 'field' }, { name: 'ops' }],
complete: false,
omitted_count: 3,
refreshed_at_ms: 1_900_000_000_500
},
greeting: greeting
}
}]
};
var profile = context._channelsHubProfileModel();
assert.strictEqual(profile.display_name, 'Ridge Operations');
assert.strictEqual(profile.authenticated_name, 'Ridge Operations');
assert.strictEqual(profile.announced_name, 'Ridge Relay');
assert.strictEqual(profile.name_mismatch, true);
assert.strictEqual(profile.destination_hash, HUB);
assert.strictEqual(profile.identity_hash, IDENTITY);
assert.strictEqual(profile.authenticated_session, true);
assert.strictEqual(profile.hops, 2);
assert.strictEqual(profile.link_mdu, 383);
assert.strictEqual(profile.greeting, greeting,
'the hub-keyed typed greeting must outrank the legacy root projection');
assert.strictEqual(profile.directory.count, 2);
assert.strictEqual(profile.directory.complete, false);
assert.strictEqual(
profile.directory.summary,
'2 public channels shown \u00b7 3 more omitted by the hub'
);
assert.strictEqual(profile.capabilities.resource_envelopes, true);
assert.strictEqual(profile.capabilities.rejoin_grace, false);
assert.strictEqual(profile.limits.max_message_body_bytes, 350);
assert.strictEqual(profile.limits.rate_messages_per_minute, 240);
context.channelsSnapshot = {
protocol_version: '0.1.3',
phase: 'stale',
hubs: [],
hub: {
destination_hash: HUB,
identity_hash: IDENTITY,
announced_name: 'Same Name',
name: 'same name',
capabilities: {},
limits: {
max_nick_bytes: null,
max_message_body_bytes: undefined
}
},
directory: {
phase: 'idle',
rooms: [],
complete: false,
omitted_count: null
},
hub_greeting: {
text: 'Short packet greeting',
delivery: 'notice',
completeness: 'unframed'
}
};
profile = context._channelsHubProfileModel();
assert.strictEqual(profile.name_mismatch, false,
'display-only name comparison should be case insensitive');
assert.strictEqual(profile.authenticated_session, false,
'an identity remembered from a stale projection is not a current Link claim');
assert.strictEqual(profile.limits.max_nick_bytes, null,
'a missing native limit must not be coerced into a zero-byte limit');
assert.strictEqual(profile.limits.max_message_body_bytes, null);
assert.strictEqual(
profile.directory.summary,
'Public channels have not been requested for this Link.'
);
assert.strictEqual(profile.greeting.delivery, 'notice');
assert.strictEqual(profile.greeting.completeness, 'unframed');
assert(modelSource.indexOf('JSON.parse') === -1);
assert(modelSource.indexOf('cbor') === -1);
assert(modelSource.indexOf('.body') === -1,
'JavaScript must consume typed native fields instead of protocol bodies');
assert(channelsSource.indexOf('Welcome & guidance') !== -1);
assert(channelsSource.indexOf(
'Complete bounded transfer \\u00b7 authenticated Link'
) !== -1);
assert(channelsSource.indexOf(
'NOTICE delivery \\u00b7 completion not signaled by this hub'
) !== -1);
assert(channelsSource.indexOf('Authenticated channel hub') !== -1);
assert(channelsSource.indexOf('Name differs from the recent announce') !== -1);
assert(channelsSource.indexOf('Private or secret channels may be intentionally hidden') !== -1);
assert(channelsSource.indexOf('Advertised by this hub in the authenticated WELCOME.') !== -1);
assert(channelsSource.indexOf("secondary.dataset.channelAction = 'hub-info'") !== -1);
assert(channelsSource.indexOf("action === 'hub-info'") !== -1);
assert(hubSource.indexOf("'Welcome & guidance'") !== -1);
assert(hubSource.indexOf('Use for rules and where to begin') !== -1);
assert(cssSource.indexOf('.channel-hub-profile-capabilities') !== -1);
assert(cssSource.indexOf('.channel-hub-greeting-delivery') !== -1);
assert(responsiveSource.indexOf('.channel-hub-profile-capabilities') !== -1);
console.log('channel hub profile tests passed');

View file

@ -966,6 +966,17 @@
padding-top: var(--space-8);
}
.channel-welcome-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-3);
}
.channel-welcome-actions .nr-btn {
min-width: 132px;
}
.channel-transition-card {
width: min(100%, 620px);
margin: 0 auto var(--space-8);
@ -1291,7 +1302,21 @@
.channel-hub-greeting.compact {
width: 100%;
margin: var(--space-6) 0;
margin: 0;
}
.channel-hub-greeting-delivery {
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid var(--border-row);
color: var(--text-muted);
font-size: var(--text-2xs);
line-height: 1.4;
}
.channel-hub-greeting[data-delivery="resource"][data-completeness="complete"]
.channel-hub-greeting-delivery {
color: var(--status-online);
}
.channel-system-event {
@ -1706,6 +1731,188 @@
line-height: var(--type-leading-copy);
}
.channel-hub-profile {
gap: var(--space-7);
}
.channel-hub-profile-hero {
padding-bottom: var(--space-6);
border-bottom: 1px solid var(--border-row);
}
.channel-hub-profile-eyebrow {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
color: var(--text-muted);
font-size: var(--text-2xs);
font-weight: var(--type-weight-semibold);
letter-spacing: var(--type-section-label-tracking);
text-transform: uppercase;
}
.channel-hub-profile-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--status-online);
box-shadow: 0 0 0 3px var(--status-online-bg);
}
.channel-hub-profile-hero[data-phase="stale"] .channel-hub-profile-status-dot,
.channel-hub-profile-hero[data-phase="reconnecting"] .channel-hub-profile-status-dot,
.channel-hub-profile-hero[data-phase="resolving"] .channel-hub-profile-status-dot,
.channel-hub-profile-hero[data-phase="connecting"] .channel-hub-profile-status-dot,
.channel-hub-profile-hero[data-phase="awaiting_welcome"] .channel-hub-profile-status-dot {
background: var(--status-warning);
box-shadow: 0 0 0 3px var(--status-warning-bg);
}
.channel-hub-profile-hero[data-phase="error"] .channel-hub-profile-status-dot,
.channel-hub-profile-hero[data-phase="offline"] .channel-hub-profile-status-dot {
background: var(--text-muted);
box-shadow: 0 0 0 3px var(--surface-control);
}
.channel-hub-profile-hero h2 {
margin: 0;
color: var(--text-primary);
font-size: var(--text-xl);
font-weight: var(--type-weight-semibold);
line-height: 1.2;
overflow-wrap: anywhere;
}
.channel-hub-profile-hero p {
margin: var(--space-3) 0 0;
color: var(--text-muted);
font-size: var(--text-xs);
line-height: 1.45;
}
.channel-hub-profile-mismatch {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-4) var(--space-5);
border-left: 3px solid var(--status-warning);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
background: var(--status-warning-bg);
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.45;
}
.channel-hub-profile-mismatch strong {
color: var(--text-primary);
font-weight: var(--type-weight-semibold);
}
.channel-hub-profile-empty-guidance {
padding: var(--space-5);
border: 1px dashed var(--border-surface-strong);
border-radius: var(--radius-lg);
color: var(--text-muted);
font-size: var(--text-xs);
line-height: 1.5;
}
.channel-hub-profile-section {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.channel-hub-profile-section h3 {
margin: 0;
color: var(--text-primary);
font-size: var(--text-sm);
font-weight: var(--type-weight-semibold);
}
.channel-hub-profile-section > p {
margin: 0;
color: var(--text-muted);
font-size: var(--text-xs);
line-height: 1.5;
}
.channel-hub-profile-details {
margin-top: var(--space-2);
}
.channel-hub-profile-detail strong.mono {
font-family: var(--font-mono);
font-size: var(--text-2xs);
user-select: text;
}
.channel-hub-profile-note {
padding-left: var(--space-4);
border-left: 2px solid var(--border-surface-strong);
}
.channel-hub-profile-capabilities {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
margin-top: var(--space-2);
}
.channel-hub-profile-capability {
display: flex;
min-width: 0;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--border-surface-soft);
border-radius: var(--radius-lg);
background: var(--surface-panel-muted);
}
.channel-hub-profile-capability-mark {
display: inline-flex;
width: 20px;
height: 20px;
flex: 0 0 20px;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--status-online-bg);
color: var(--status-online);
font-size: var(--text-xs);
font-weight: var(--type-weight-semibold);
}
.channel-hub-profile-capability[data-enabled="false"] {
background: transparent;
}
.channel-hub-profile-capability[data-enabled="false"]
.channel-hub-profile-capability-mark {
background: var(--surface-control);
color: var(--text-muted);
}
.channel-hub-profile-capability-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.channel-hub-profile-capability-copy strong {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: var(--type-weight-semibold);
}
.channel-hub-profile-capability-copy > span {
color: var(--text-muted);
font-size: var(--text-2xs);
line-height: 1.4;
}
.channel-share-qr-shell {
display: flex;
width: min(100%, 292px);

View file

@ -3730,6 +3730,28 @@
padding: var(--space-4) var(--space-5);
}
.channel-welcome-actions {
width: 100%;
}
.channel-welcome-actions .nr-btn {
min-height: 42px;
flex: 1 1 132px;
}
.channel-hub-profile-capabilities {
grid-template-columns: 1fr;
}
.channel-hub-profile-hero h2 {
font-size: var(--text-lg);
}
.channel-hub-profile-detail {
grid-template-columns: minmax(82px, auto) minmax(0, 1fr);
gap: var(--space-3);
}
.channel-welcome-mark {
width: 68px;
height: 68px;

View file

@ -2078,9 +2078,13 @@ function channelHubOpenManager(initialOverview) {
greetingInput.className = 'nr-input-sm channel-host-greeting';
greetingInput.maxLength = 512;
greetingInput.rows = 3;
greetingInput.placeholder = 'Welcome people and tell them where to begin';
greetingInput.placeholder = 'Welcome people, share rules, and tell them where to begin';
greetingInput.value = settings.greeting || '';
var greetingField = _channelHubField('Welcome message', greetingInput, 'Shown once when someone connects');
var greetingField = _channelHubField(
'Welcome & guidance',
greetingInput,
'Shown after WELCOME when someone connects'
);
var greetingCount = greetingField.querySelector('.channel-host-field-hint');
profile.appendChild(greetingField);
panels.settings.appendChild(profile);
@ -2426,7 +2430,8 @@ function channelHubOpenManager(initialOverview) {
save.disabled = busy || adminMutationBusy || !dirty || !nameInput.value.trim();
save.hidden = activeTab !== 'settings';
impact.hidden = !(dirty && overview.status && overview.status.running);
greetingCount.textContent = (greetingInput.value.length || 0) + '/512 · Shown once when someone connects';
greetingCount.textContent = (greetingInput.value.length || 0) +
'/512 \u00b7 Use for rules and where to begin';
}
function setBusy(value, label) {

View file

@ -613,6 +613,114 @@ function _channelsHubSwitcherModel() {
};
}
function _channelsCurrentHubObserved() {
var destination = String(
(channelsSnapshot.hub && channelsSnapshot.hub.destination_hash) ||
channelsSnapshot.selected_hub_destination ||
''
).toLowerCase();
var hubs = Array.isArray(channelsSnapshot.hubs) ? channelsSnapshot.hubs : [];
for (var i = 0; i < hubs.length; i++) {
if (String(hubs[i].destination_hash || '').toLowerCase() !== destination) continue;
return hubs[i].observed || null;
}
return null;
}
// A presentation-safe view of authenticated native state. This intentionally
// interprets no RRC payloads in JavaScript: WELCOME, directory, and greeting
// provenance have already crossed the native validation boundary as typed
// fields.
function _channelsHubProfileModel() {
var observed = _channelsCurrentHubObserved();
var hub = (observed && observed.hub) || channelsSnapshot.hub || {};
var directory = (observed && observed.directory) ||
channelsSnapshot.directory || {};
var greeting = (observed && observed.greeting) ||
channelsSnapshot.hub_greeting || null;
var authenticatedName = String(hub.name || '').trim();
var announcedName = String(hub.announced_name || '').trim();
var destination = String(hub.destination_hash || '').toLowerCase();
var directoryRooms = Array.isArray(directory.rooms) ? directory.rooms : [];
var omitted = Number.isSafeInteger(Number(directory.omitted_count))
? Math.max(0, Number(directory.omitted_count)) : 0;
var directoryPhase = String(directory.phase || 'idle');
var directorySummary;
if (directoryPhase === 'loading') {
directorySummary = 'Refreshing public channels\u2026';
} else if (directoryPhase === 'error') {
directorySummary = directory.last_error ||
'Public channel information is unavailable.';
} else if (directoryPhase !== 'ready') {
directorySummary = 'Public channels have not been requested for this Link.';
} else if (!directoryRooms.length && directory.complete) {
directorySummary = 'No public channels were advertised.';
} else {
directorySummary = directoryRooms.length + ' public ' +
(directoryRooms.length === 1 ? 'channel' : 'channels');
if (!directory.complete) {
directorySummary += omitted
? ' shown \u00b7 ' + omitted + ' more omitted by the hub'
: ' shown \u00b7 response may be incomplete';
}
}
function safeLimit(value) {
if (value === null || value === undefined || value === '') return null;
value = Number(value);
return Number.isSafeInteger(value) && value >= 0 ? value : null;
}
var capabilities = hub.capabilities || {};
var limits = hub.limits || {};
var phase = String((observed && observed.phase) ||
channelsSnapshot.phase || 'offline');
var identityHash = String(hub.identity_hash || '').toLowerCase();
var connectedAt = safeLimit(hub.connected_at_ms);
return {
hub: hub,
destination_hash: destination,
identity_hash: identityHash,
authenticated_name: authenticatedName || null,
announced_name: announcedName || null,
display_name: authenticatedName || announcedName ||
(destination ? 'Channel hub ' + destination.slice(0, 8) : 'Channel hub'),
name_mismatch: !!(authenticatedName && announcedName &&
authenticatedName.toLowerCase() !== announcedName.toLowerCase()),
phase: phase,
authenticated_session: !!(identityHash && connectedAt != null &&
(phase === 'active' || phase === 'stale')),
nickname: (observed && observed.nickname) || channelsSnapshot.nickname || null,
hops: safeLimit(hub.hops),
link_mdu: safeLimit(hub.link_mdu),
connected_at_ms: connectedAt,
hub_version: hub.version || null,
protocol_version: channelsSnapshot.protocol_version || null,
greeting: greeting,
directory: {
phase: directoryPhase,
count: directoryRooms.length,
complete: !!directory.complete,
omitted_count: omitted,
refreshed_at_ms: safeLimit(directory.refreshed_at_ms),
summary: directorySummary
},
capabilities: {
actions: !!capabilities.actions,
direct_notices: !!capabilities.direct_notices,
resource_envelopes: !!capabilities.resource_envelopes,
rejoin_grace: !!capabilities.rejoin_grace
},
limits: {
max_nick_bytes: safeLimit(limits.max_nick_bytes),
max_room_name_bytes: safeLimit(limits.max_room_name_bytes),
max_message_body_bytes: safeLimit(limits.max_message_body_bytes),
max_rooms_per_session: safeLimit(limits.max_rooms_per_session),
rate_messages_per_minute: safeLimit(limits.rate_messages_per_minute)
}
};
}
function _channelsConnectCommandBlocked() {
return channelsSnapshot.phase === 'resolving' ||
channelsSnapshot.phase === 'connecting' ||
@ -2095,6 +2203,7 @@ function _channelsRenderRoomEmpty(transcript) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'nr-btn nr-btn-primary';
var secondary = null;
if (_channelsIsConnecting()) {
title.textContent = _channelsPhaseLabel(channelsSnapshot.phase) + '\u2026';
@ -2103,12 +2212,24 @@ function _channelsRenderRoomEmpty(transcript) {
button.dataset.channelAction = 'disconnect';
button.textContent = 'Cancel';
} else if (_channelsIsConnected()) {
title.textContent = 'Connected. Join a channel.';
copy.textContent = 'Choose a channel on ' + _channelsHubName(channelsSnapshot.hub) + '.';
var profile = _channelsHubProfileModel();
state.classList.add('channel-hub-home-state');
title.textContent = profile.display_name;
copy.textContent = 'Authenticated Reticulum Link' +
(profile.hops == null
? ''
: ' \u00b7 ' + profile.hops +
(profile.hops === 1 ? ' hop' : ' hops')) +
'. ' + profile.directory.summary;
button.dataset.channelAction = 'join';
button.textContent = 'Join a channel';
if (channelsSnapshot.hub_greeting) {
transcript.appendChild(_channelsBuildHubGreeting(channelsSnapshot.hub_greeting));
button.textContent = 'Browse & join';
secondary = document.createElement('button');
secondary.type = 'button';
secondary.className = 'nr-btn nr-btn-secondary';
secondary.dataset.channelAction = 'hub-info';
secondary.textContent = 'Hub information';
if (profile.greeting) {
transcript.appendChild(_channelsBuildHubGreeting(profile.greeting));
state.classList.add('has-hub-greeting');
}
} else if (channelsSnapshot.phase === 'error') {
@ -2132,19 +2253,28 @@ function _channelsRenderRoomEmpty(transcript) {
state.appendChild(mark);
state.appendChild(title);
state.appendChild(copy);
state.appendChild(button);
var actions = document.createElement('div');
actions.className = 'channel-welcome-actions';
actions.appendChild(button);
if (secondary) actions.appendChild(secondary);
state.appendChild(actions);
transcript.appendChild(state);
}
function _channelsBuildHubGreeting(item, compact) {
var greeting = document.createElement('aside');
greeting.className = 'channel-hub-greeting' + (compact ? ' compact' : '');
greeting.setAttribute('aria-label', 'Hub greeting');
greeting.dataset.delivery = item.delivery || 'notice';
greeting.dataset.completeness = item.completeness || 'unframed';
greeting.setAttribute(
'aria-label',
'Welcome and guidance from the authenticated channel hub'
);
var heading = document.createElement('div');
heading.className = 'channel-hub-greeting-heading';
var label = document.createElement('span');
label.className = 'channel-hub-notice-label';
label.textContent = 'Hub greeting';
label.textContent = 'Welcome & guidance';
var hub = document.createElement('span');
hub.className = 'channel-hub-greeting-source';
hub.textContent = _channelsHubName(channelsSnapshot.hub);
@ -2155,6 +2285,14 @@ function _channelsBuildHubGreeting(item, compact) {
heading.appendChild(hub);
greeting.appendChild(heading);
greeting.appendChild(body);
var delivery = document.createElement('div');
delivery.className = 'channel-hub-greeting-delivery';
if (item.delivery === 'resource' && item.completeness === 'complete') {
delivery.textContent = 'Complete bounded transfer \u00b7 authenticated Link';
} else {
delivery.textContent = 'NOTICE delivery \u00b7 completion not signaled by this hub';
}
greeting.appendChild(delivery);
return greeting;
}
@ -3868,27 +4006,251 @@ function channelsOpenJoinSheet(prefillRoom) {
_channelsPresentSheet(built, roomInput);
}
function _channelsHubProfileDetail(labelText, value, mono) {
var row = _channelsRoomDetail(labelText, value);
row.classList.add('channel-hub-profile-detail');
if (mono) row.querySelector('strong').classList.add('mono');
return row;
}
function _channelsHubProfileSection(titleText, copyText) {
var section = document.createElement('section');
section.className = 'channel-hub-profile-section';
var title = document.createElement('h3');
title.textContent = titleText;
section.appendChild(title);
if (copyText) {
var copy = document.createElement('p');
copy.textContent = copyText;
section.appendChild(copy);
}
return section;
}
function _channelsHubCapability(labelText, description, enabled) {
var capability = document.createElement('div');
capability.className = 'channel-hub-profile-capability';
capability.dataset.enabled = enabled ? 'true' : 'false';
var mark = document.createElement('span');
mark.className = 'channel-hub-profile-capability-mark';
mark.setAttribute('aria-hidden', 'true');
mark.textContent = enabled ? '\u2713' : '\u2014';
var copy = document.createElement('span');
copy.className = 'channel-hub-profile-capability-copy';
var label = document.createElement('strong');
label.textContent = labelText;
var detail = document.createElement('span');
detail.textContent = enabled ? description : 'Not advertised by this hub';
copy.appendChild(label);
copy.appendChild(detail);
capability.appendChild(mark);
capability.appendChild(copy);
return capability;
}
function channelsOpenHubOptions() {
if (!channelsSnapshot.hub || typeof _rsBuildSheet !== 'function') return;
var hub = channelsSnapshot.hub;
var built = _rsBuildSheet({ title: _channelsHubName(hub) }, function() {});
var copy = document.createElement('p');
copy.className = 'channel-sheet-copy mono';
copy.textContent = hub.destination_hash;
built.body.appendChild(copy);
var details = document.createElement('p');
details.className = 'channel-sheet-copy';
var parts = [_channelsPhaseLabel(channelsSnapshot.phase)];
if (hub.hops != null) parts.push(hub.hops + (hub.hops === 1 ? ' hop' : ' hops'));
if (hub.version) parts.push('Hub ' + hub.version);
details.textContent = parts.join(' \u00b7 ');
built.body.appendChild(details);
if (channelsSnapshot.hub_greeting) {
built.body.appendChild(_channelsBuildHubGreeting(channelsSnapshot.hub_greeting, true));
var profile = _channelsHubProfileModel();
var hub = profile.hub;
var built = _rsBuildSheet({ title: 'Hub information' }, function() {});
built.body.classList.add('channel-hub-profile');
var hero = document.createElement('header');
hero.className = 'channel-hub-profile-hero';
hero.dataset.phase = profile.phase;
var eyebrow = document.createElement('div');
eyebrow.className = 'channel-hub-profile-eyebrow';
var statusDot = document.createElement('span');
statusDot.className = 'channel-hub-profile-status-dot';
statusDot.setAttribute('aria-hidden', 'true');
var status = document.createElement('span');
status.textContent = profile.authenticated_session
? 'Authenticated channel hub'
: 'Channel hub connection';
eyebrow.appendChild(statusDot);
eyebrow.appendChild(status);
var name = document.createElement('h2');
name.textContent = profile.display_name;
var summary = document.createElement('p');
var summaryParts = [_channelsPhaseLabel(profile.phase)];
if (profile.hops != null) {
summaryParts.push(profile.hops + (profile.hops === 1 ? ' hop' : ' hops'));
}
if (profile.hub_version) summaryParts.push('Hub software ' + profile.hub_version);
summary.textContent = summaryParts.join(' \u00b7 ');
hero.appendChild(eyebrow);
hero.appendChild(name);
hero.appendChild(summary);
built.body.appendChild(hero);
if (profile.name_mismatch) {
var mismatch = document.createElement('div');
mismatch.className = 'channel-hub-profile-mismatch';
mismatch.setAttribute('role', 'note');
var mismatchTitle = document.createElement('strong');
mismatchTitle.textContent = 'Name differs from the recent announce';
var mismatchCopy = document.createElement('span');
mismatchCopy.textContent = 'The announce said \u201c' + profile.announced_name +
'\u201d; this authenticated WELCOME says \u201c' +
profile.authenticated_name + '\u201d.';
mismatch.appendChild(mismatchTitle);
mismatch.appendChild(mismatchCopy);
built.body.appendChild(mismatch);
}
if (profile.greeting) {
built.body.appendChild(_channelsBuildHubGreeting(profile.greeting, true));
} else {
var noGuidance = document.createElement('div');
noGuidance.className = 'channel-hub-profile-empty-guidance';
noGuidance.textContent =
'This hub did not send welcome or rules guidance for this Link session.';
built.body.appendChild(noGuidance);
}
var identitySection = _channelsHubProfileSection(
'Connection identity',
profile.authenticated_session
? 'These values come from the destination and authenticated Link, not from a share label.'
: 'Ratspeak has no active authenticated WELCOME for this hub right now.'
);
var identityDetails = document.createElement('div');
identityDetails.className = 'channel-room-details channel-hub-profile-details';
identityDetails.appendChild(_channelsHubProfileDetail(
'Destination',
profile.destination_hash || 'Unavailable',
true
));
identityDetails.appendChild(_channelsHubProfileDetail(
'Hub identity',
profile.identity_hash || 'Unavailable',
true
));
if (profile.authenticated_name) {
identityDetails.appendChild(_channelsHubProfileDetail(
'WELCOME name',
profile.authenticated_name
));
}
if (profile.announced_name) {
identityDetails.appendChild(_channelsHubProfileDetail(
'Recent announce',
profile.announced_name
));
}
if (profile.nickname) {
identityDetails.appendChild(_channelsHubProfileDetail(
'Your nickname',
profile.nickname
));
}
identityDetails.appendChild(_channelsHubProfileDetail(
'Path',
profile.hops == null
? 'Hop count unavailable'
: profile.hops + (profile.hops === 1 ? ' hop' : ' hops')
));
if (profile.link_mdu != null) {
identityDetails.appendChild(_channelsHubProfileDetail(
'Link MDU',
profile.link_mdu + ' bytes'
));
}
if (profile.protocol_version) {
identityDetails.appendChild(_channelsHubProfileDetail(
'RRC profile',
profile.protocol_version
));
}
identitySection.appendChild(identityDetails);
built.body.appendChild(identitySection);
var directorySection = _channelsHubProfileSection(
'Public directory',
profile.directory.summary
);
var directoryNote = document.createElement('p');
directoryNote.className = 'channel-hub-profile-note';
directoryNote.textContent =
'Only public channels disclosed on this Link appear here. Private or secret channels may be intentionally hidden.';
directorySection.appendChild(directoryNote);
built.body.appendChild(directorySection);
var capabilitySection = _channelsHubProfileSection(
'Session capabilities',
'Advertised by this hub in the authenticated WELCOME.'
);
var capabilityGrid = document.createElement('div');
capabilityGrid.className = 'channel-hub-profile-capabilities';
capabilityGrid.appendChild(_channelsHubCapability(
'Action messages',
'Supports action-style channel posts',
profile.capabilities.actions
));
capabilityGrid.appendChild(_channelsHubCapability(
'Direct notices',
'Can address a hub notice to one session',
profile.capabilities.direct_notices
));
capabilityGrid.appendChild(_channelsHubCapability(
'Complete welcome transfer',
'Can deliver bounded welcome guidance as a Resource',
profile.capabilities.resource_envelopes
));
capabilityGrid.appendChild(_channelsHubCapability(
'Invite rejoin grace',
'May restore a short identity-bound invite after reconnect',
profile.capabilities.rejoin_grace
));
capabilitySection.appendChild(capabilityGrid);
built.body.appendChild(capabilitySection);
var limitRows = [];
if (profile.limits.max_message_body_bytes != null) {
limitRows.push(['Message body', profile.limits.max_message_body_bytes + ' UTF-8 bytes']);
}
if (profile.limits.max_rooms_per_session != null) {
limitRows.push([
'Joined channels',
profile.limits.max_rooms_per_session + ' per session'
]);
}
if (profile.limits.rate_messages_per_minute != null) {
limitRows.push([
'Message rate',
profile.limits.rate_messages_per_minute + ' per minute'
]);
}
if (profile.limits.max_nick_bytes != null) {
limitRows.push(['Nickname', profile.limits.max_nick_bytes + ' UTF-8 bytes']);
}
if (profile.limits.max_room_name_bytes != null) {
limitRows.push([
'Channel name',
profile.limits.max_room_name_bytes + ' UTF-8 bytes'
]);
}
var limitsSection = _channelsHubProfileSection(
'Hub limits',
limitRows.length
? 'Applied by the hub to this session.'
: 'This hub did not advertise concrete session limits.'
);
if (limitRows.length) {
var limitDetails = document.createElement('div');
limitDetails.className = 'channel-room-details channel-hub-profile-details';
limitRows.forEach(function(row) {
limitDetails.appendChild(_channelsHubProfileDetail(row[0], row[1]));
});
limitsSection.appendChild(limitDetails);
}
built.body.appendChild(limitsSection);
var trust = document.createElement('div');
trust.className = 'channel-sheet-trust-note';
trust.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg><span>This hub is authenticated and the Link is encrypted. The hub can still read and relay everything posted to its channels.</span>';
trust.innerHTML = profile.authenticated_session
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg><span>The destination and Link are authenticated and encrypted. The hub operator can still read, moderate, and relay everything posted to its channels.</span>'
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg><span>Ratspeak authenticates the destination and Link before any channel action. This view is not proof of an active session.</span>';
built.body.appendChild(trust);
var copyButton = document.createElement('button');
@ -4204,6 +4566,7 @@ function _channelsBindUI() {
if (action === 'connect') channelsOpenConnectSheet();
else if (action === 'open-owned-hub' && typeof channelHubOpenOwnHub === 'function') channelHubOpenOwnHub();
else if (action === 'join') channelsOpenJoinSheet();
else if (action === 'hub-info') channelsOpenHubOptions();
else if (action === 'disconnect') channelsDisconnect();
else if (action === 'retry-room') channelsOpenJoinSheet(actionEl.dataset.room || '');
else if (action === 'leave-room') {