OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 11d835e

0.0.54: discover — the step between finding someone and seeing them
⛔ A TIMELINE IS BUILT ONLY FROM FEEDS YOU FOLLOW, and nothing anywhere offered a follow button next to a name. So the honest sequence was: join a namespace, search a handle, see a result, and stop — two steps from a timeline with no visible route. That is indistinguishable from a broken network and was reported as one. `/discover` lists every claim in the current namespace with a follow on each, and sits beside `search` in the nav. The pair is the point: search answers "where is this person I can name", discover answers "who is here at all", and only the first question could be asked before. ⛔ NO QUERY, DELIBERATELY. `/search` returns without asking the daemon when the box is empty, which is right there — an empty query is not a request for the whole world. A namespace is the one scope where "show me everyone" IS the question. ⛔ `unheld` IS LISTED, labelled. It means "this claim exists and I do not hold the feed to check it against", which is the normal state for everyone you have not followed — hiding it would empty the very page that exists to find them. MISMATCH is hidden: that claim points at someone else's feed. ⛔ THE EMPTY STATE NAMES THE NAMESPACE. "Nobody is here" and "you are in the wrong namespace" look identical otherwise, and the second is the likely one — an invite token and a namespace key are both 64 hex characters, so joining the wrong thing succeeds silently. That bug is still unfixed; this at least makes it diagnosable from the page where it shows up. ⚠ Most rows will have no display name, and that is not a defect: profiles come from feeds this node HOLDS, so a stranger's display name is unreachable until you follow them. The handle is what identifies them until then, which is why it leads each row. 188/188 tests, 601/601 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11d835ecfb9f79d8a9b44c714e6f765bf6aad388
by plankatron <fleeky@gmail.com> · 2026-09-10T15:55:32.000Z

Changed files (4) +113 -3

lib/pages.js +59 -1
……@@ -565,7 +565,11 @@ function oneLine (text, max) {
565565 * ⛔ The nav marks where you ARE. Eight identical underlined links told a reader nothing about which
566566 * page they were on, which is most of what a navigation bar is for.
567567 */
568const NAV_ITEMS = [['/', 'timeline'], ['/rooms', 'rooms'], ['/search', 'search'], ['/following', 'following'],
568// ⛔ `discover` SITS BESIDE `search`, and the pair is deliberate. Search answers "where is this
569// person I can name"; discover answers "who is here at all" — and until it existed only the first
570// question could be asked, so joining a namespace and then finding nobody was the whole experience.
571const NAV_ITEMS = [['/', 'timeline'], ['/rooms', 'rooms'], ['/search', 'search'], ['/discover', 'discover'],
572 ['/following', 'following'],
569573 ['/notifications', 'notifications'], ['/mentions', 'mentions'], ['/knocks', 'knocks'],
570574 ['/graph', 'graph'], ['/explore', 'explore'],
571575 ['/settings', 'settings'], ['/logout', 'sign out']]
……@@ -1663,3 +1667,57 @@ export function minted (mnemonic, restored = false) {
16631667 " catch { document.getElementById('copied').textContent = 'copy failed — select the words above' }\n" +
16641668 '})\n</script>')
16651669}
1670
1671// ── discovery ────────────────────────────────────────────────────────────────────────────────────
1672//
1673// ⛔ THE STEP NOBODY COULD TAKE. Joining a namespace resolves handles; it does not make you follow
1674// anyone, and a timeline is built ONLY from feeds you follow. So a person could join the fleet's
1675// namespace, search correctly, see a handle, and still have no route from "found them" to "seeing
1676// them" — there was no follow button anywhere near a search result. Two steps from a timeline with
1677// no visible path is indistinguishable from a broken network, and that is what it was reported as.
1678//
1679// ⛔ AND IT SHOWS EVERYONE BY DEFAULT. `/search` returns without asking the daemon when the box is
1680// empty — deliberate there, because an empty query is not a request for the whole world. A
1681// namespace is exactly the scope where "show me everyone" IS the question, so this asks with no
1682// query and gets `names.list()`.
1683export function discover (handles, following = [], profiles = {}, ns = null) {
1684 const held = new Set((following || []).map((f) => (typeof f === 'string' ? f : f && f.feed)).filter(Boolean))
1685 const rows = (handles || []).filter((h) => h.verdict !== 'MISMATCH')
1686
1687 const key = ns && ns.key
1688 ? '<p class="who">Namespace <code>' + esc(String(ns.key).slice(0, 16)) + '…</code></p>'
1689 : ''
1690
1691 if (!rows.length) {
1692 // ⛔ NAMES THE NAMESPACE IN THE EMPTY STATE. "Nobody is here" and "you are in the wrong
1693 // namespace" look identical otherwise, and the second is the likely one — an invite token and a
1694 // namespace key are both 64 hex characters, so joining the wrong thing succeeds silently.
1695 return layout('discover', '<h1>Nobody here yet</h1>' + key +
1696 '<p>No handles have been claimed in this namespace. If you expected people, check you are in ' +
1697 'the right one — a namespace key and an invite token are both 64 characters, and joining the ' +
1698 'wrong one looks like this.</p>' +
1699 '<p><a href="/settings">namespace settings</a></p>')
1700 }
1701
1702 const row = (h) => {
1703 const p = (profiles || {})[h.feed] || {}
1704 const name = p.name ? '<strong>' + esc(p.name) + '</strong> ' : ''
1705 // ⚠ `unheld` is LABELLED, not hidden. It means "this claim exists and I do not hold the feed to
1706 // check it against" — which is the normal state for everyone you have not followed yet, so
1707 // hiding it would empty the very page that exists to find them.
1708 const flag = h.verdict === 'confirmed' ? '' : ' <span class="who">[' + esc(h.verdict) + ']</span>'
1709 const action = held.has(h.feed)
1710 ? '<span class="who">following</span>'
1711 : '<form method="post" action="/follow" class="inline">' +
1712 '<input type="hidden" name="feed" value="' + esc(h.feed) + '"><button>follow</button></form>'
1713 return '<li>' + name + '@' + esc(h.handle) + flag +
1714 // ⛔ The key is always shown. A handle is a claim; the key is the unforgeable thing, and this
1715 // page's whole job is handing someone a feed to follow.
1716 ' <code class="who">' + esc(String(h.feed).slice(0, 8)) + '</code> ' + action + '</li>'
1717 }
1718
1719 return layout('discover', '<h1>People here</h1>' + key +
1720 '<p class="who">Everyone who has claimed a handle in this namespace. Following one is what puts ' +
1721 'their posts in your timeline — joining a namespace on its own shows you nothing.</p>' +
1722 '<ul class="ns-list">' + rows.map(row).join('') + '</ul>')
1723}
lib/router.js +15 -0
……@@ -619,6 +619,21 @@ export async function route (req, res, ctx) {
619619 return send(res, pages.mentions(rows, profiles, await fileTypes(rows, client)))
620620 }
621621
622 // ⛔ NO QUERY, DELIBERATELY. `/search` returns without asking the daemon when the box is empty,
623 // which is right there — an empty query is not a request for the whole world. A namespace is the
624 // one scope where "show me everyone" IS the question, so this asks with none and gets the full
625 // claim list. Without it, joining a namespace and then finding nobody was the reported experience.
626 if (req.method === 'GET' && path === '/discover') {
627 const named = await client.call('search-names', {}).catch(() => ({ handles: [] }))
628 const ns = await client.call('namespace').catch(() => null)
629 // Profiles are only available for feeds this node HOLDS, so most rows here have none — a
630 // stranger's display name is unreachable until you follow them. The handle is what identifies
631 // them until then, which is why it leads each row.
632 const profiles = await client.call('profiles').catch(() => ({}))
633 const following = await client.call('following').catch(() => [])
634 return send(res, pages.discover(named.handles || [], following, profiles, ns))
635 }
636
622637 if (req.method === 'GET' && path === '/following') {
623638 // ⛔ Profiles too. A following list that showed keys could not say WHO you follow, which is the
624639 // only question that page answers.
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.53",
3 "version": "0.0.54",
44 "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
55 "type": "module",
66 "main": "index.js",
test/pages.test.js +38 -1
……@@ -2,7 +2,7 @@
22
33import test from 'brittle'
44import fs from '#node:fs'
5import { timeline, search, following, knocks, post, esc, layout, who, person, settings, handleSettings, namespaceSettings, composer, mentions, notifications, thread as pages_thread, withUpdateBanner, updateSettings } from '../lib/pages.js'
5import { timeline, search, following, knocks, post, esc, layout, who, person, settings, handleSettings, namespaceSettings, composer, mentions, notifications, thread as pages_thread, withUpdateBanner, updateSettings, discover } from '../lib/pages.js'
66
77const row = (over = {}) => ({
88 author: 'ab'.repeat(32), collection: 'post', name: 'p1',
……@@ -615,3 +615,40 @@ test('★ the update section names the running VERSION, not just tree hashes', (
615615 t.ok(old.includes('predates the stamp'), 'and an unstamped build says why it cannot tell you')
616616 t.absent(old.includes('Running undefined'), 'never a fabricated version')
617617})
618
619// ── discovery ────────────────────────────────────────────────────────────────────────────────────
620//
621// ⛔ The page exists because joining a namespace and then finding nobody was the whole reported
622// experience: a timeline is built only from feeds you FOLLOW, and nothing offered a follow button
623// next to a name. These pin the parts that made it useless before.
624test('★ discover lists everyone and offers a FOLLOW for each — the step that was missing', (t) => {
625 const html = discover(
626 [{ handle: 'tabertron0', feed: 'a'.repeat(64), verdict: 'unheld' },
627 { handle: 'stortron0', feed: 'b'.repeat(64), verdict: 'confirmed' }],
628 [], {}, { key: '2c'.repeat(32) })
629 t.ok(html.includes('@tabertron0') && html.includes('@stortron0'), 'both are listed')
630 t.is((html.match(/action="\/follow"/g) || []).length, 2, 'a follow button on each')
631 // ⛔ `unheld` is the NORMAL state for anyone you have not followed. Hiding it would empty the very
632 // page that exists to find them.
633 t.ok(html.includes('unheld'), 'unheld is labelled, not hidden')
634})
635
636test('someone already followed gets no redundant button', (t) => {
637 const feed = 'a'.repeat(64)
638 const html = discover([{ handle: 'tabertron0', feed, verdict: 'confirmed' }], [{ feed }], {}, null)
639 t.ok(html.includes('following'), 'it says so')
640 t.absent(html.includes('action="/follow"'), 'and does not offer to follow again')
641})
642
643test('⛔ a MISMATCH is never offered — that claim points at someone else\'s feed', (t) => {
644 const html = discover([{ handle: 'imposter0', feed: 'c'.repeat(64), verdict: 'MISMATCH' }], [], {}, null)
645 t.absent(html.includes('imposter0'), 'a claim whose feed disagrees is not a person to follow')
646})
647
648// ⛔ "Nobody is here" and "you are in the WRONG namespace" look identical otherwise — and the second
649// is the likely one, because an invite token and a namespace key are both 64 hex characters.
650test('the empty state names the namespace, so a wrong one is diagnosable', (t) => {
651 const html = discover([], [], {}, { key: 'ab'.repeat(32) })
652 t.ok(html.includes('Nobody here yet'))
653 t.ok(html.includes('abababab'), 'the key is shown so you can tell which namespace you are in')
654})