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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions site/beacon/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
</div>
</div>

<!-- Live agent rankings (Track C: agent leaderboard) -->
<aside class="agent-leaderboard" aria-label="Top Beacon agents" aria-live="polite">
<div class="leaderboard-empty">Loading agent activity...</div>
</aside>

<!-- Controls hint (bottom-left) -->
<div class="controls-hint">
DRAG rotate | SCROLL zoom | RIGHT-DRAG pan<br>
Expand Down
105 changes: 105 additions & 0 deletions site/beacon/leaderboard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT

const MAX_METRIC = 1_000_000_000;
const DEFAULT_LIMIT = 10;
const MAX_LIMIT = 50;

function safeCount(value) {
const count = Number(value);
if (!Number.isFinite(count) || count <= 0) return 0;
return Math.min(MAX_METRIC, Math.floor(count));
}

function safeLimit(value) {
const limit = Number(value);
if (!Number.isFinite(limit) || limit <= 0) return DEFAULT_LIMIT;
return Math.min(MAX_LIMIT, Math.floor(limit));
}

function reputationFor(reputation, agentId) {
if (reputation instanceof Map) return reputation.get(agentId) || {};
if (!reputation || typeof reputation !== 'object') return {};
return reputation[agentId] || {};
}

function visibleContractCounts(contracts) {
const counts = new Map();
const seenByAgent = new Map();

for (const [index, contract] of (Array.isArray(contracts) ? contracts : []).entries()) {
if (!contract || typeof contract !== 'object') continue;
const contractKey = String(contract.id ?? `row-${index}`);
const participants = new Set([contract.from, contract.to].filter(value => (
typeof value === 'string' && value.length > 0
)));

for (const agentId of participants) {
if (!seenByAgent.has(agentId)) seenByAgent.set(agentId, new Set());
const seen = seenByAgent.get(agentId);
if (seen.has(contractKey)) continue;
seen.add(contractKey);
counts.set(agentId, (counts.get(agentId) || 0) + 1);
}
}

return counts;
}

function compareText(left, right) {
const a = String(left ?? '').toLowerCase();
const b = String(right ?? '').toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
}

/**
* Build a deterministic leaderboard without mutating live Atlas data.
*
* Contract totals use the larger of currently-visible relationships and the
* backend's completed-contract counter so historical work remains represented.
*/
export function buildAgentLeaderboard(
agents,
contracts,
reputation = {},
sortBy = 'beats',
limit = DEFAULT_LIMIT,
) {
const contractCounts = visibleContractCounts(contracts);
const entries = [];
const seenAgentIds = new Set();

for (const agent of (Array.isArray(agents) ? agents : [])) {
if (!agent || typeof agent !== 'object') continue;
const id = typeof agent.id === 'string' ? agent.id : '';
if (!id || seenAgentIds.has(id)) continue;
seenAgentIds.add(id);

const rep = reputationFor(reputation, id);
const beats = safeCount(agent.beat_count);
const contractCount = Math.max(
safeCount(contractCounts.get(id)),
safeCount(rep.contracts_completed),
);

if (beats === 0 && contractCount === 0) continue;
entries.push({
id,
name: typeof agent.name === 'string' && agent.name.trim() ? agent.name.trim() : id,
beats,
contracts: contractCount,
});
}

const mode = sortBy === 'contracts' ? 'contracts' : 'beats';
const secondary = mode === 'beats' ? 'contracts' : 'beats';
entries.sort((a, b) => (
b[mode] - a[mode]
|| b[secondary] - a[secondary]
|| compareText(a.name, b.name)
|| compareText(a.id, b.id)
));

return entries.slice(0, safeLimit(limit));
}
130 changes: 130 additions & 0 deletions site/beacon/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,125 @@ html, body {
color: var(--green);
}

/* --- Agent Leaderboard (left sidebar) --- */
.agent-leaderboard {
position: fixed;
top: 150px;
left: 16px;
z-index: 50;
width: min(340px, calc(100vw - 32px));
max-height: calc(100vh - 220px);
display: flex;
flex-direction: column;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.05);
color: var(--text-body);
font-family: var(--font-mono);
pointer-events: auto;
overflow: hidden;
}

.leaderboard-header {
padding: 8px 10px;
background: rgba(0, 20, 0, 0.65);
border-bottom: 1px solid var(--border);
}

.leaderboard-title {
color: var(--amber);
font-family: var(--font-display);
font-size: 20px;
font-weight: 400;
letter-spacing: 1px;
}

.leaderboard-controls {
display: flex;
gap: 6px;
margin-top: 5px;
}

.leaderboard-mode {
padding: 2px 7px;
background: transparent;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 10px;
cursor: pointer;
}

.leaderboard-mode[aria-pressed="true"] {
border-color: var(--amber-dim);
color: var(--amber);
background: rgba(255, 176, 0, 0.08);
}

.leaderboard-list {
min-height: 34px;
margin: 0;
padding: 0;
list-style: none;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--green-dim) transparent;
}

.leaderboard-item {
margin: 0;
padding: 0;
}

.leaderboard-row {
width: 100%;
display: grid;
grid-template-columns: 26px minmax(0, 1fr) 54px 54px;
gap: 5px;
align-items: center;
padding: 6px 9px;
background: transparent;
border: 0;
border-bottom: 1px solid rgba(26, 51, 26, 0.65);
color: var(--text-body);
font-family: var(--font-mono);
font-size: 11px;
text-align: left;
cursor: pointer;
}

.leaderboard-row:hover,
.leaderboard-row:focus-visible {
background: rgba(51, 255, 51, 0.08);
color: var(--green);
outline: 1px solid var(--green-dim);
outline-offset: -1px;
}

.leaderboard-rank {
color: var(--amber);
}

.leaderboard-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.leaderboard-metric {
color: var(--text-dim);
text-align: right;
font-variant-numeric: tabular-nums;
}

.leaderboard-empty {
padding: 10px;
color: var(--text-dim);
font-size: 11px;
}

/* --- Controls hint (bottom-left) --- */
.controls-hint {
position: fixed;
Expand Down Expand Up @@ -753,6 +872,17 @@ html, body {
.hud-title { font-size: 22px; }
.hud-stats { font-size: 14px; }

.agent-leaderboard {
top: 132px;
left: 8px;
width: min(330px, calc(100vw - 16px));
max-height: 36vh;
}

.leaderboard-row {
padding: 5px 7px;
}

.controls-hint { display: none; }
}

Expand Down
Loading