Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#151

Give every account a profile page and a directory of people

merged · by keenan 8d ago · merged into main at 4739664b by keenan 7d ago

keenan opened this 8d ago

Names are everywhere in the forge and none of them could be clicked. This adds /u/<name>, a directory at /u, and links from every name a credential produced.

Closes #150.

What is on a profile

Issues opened, changes opened, reviews given, and comments, then the repositories that work is in, then the activity feed filtered to that account. The four numbers are links to the feed they count, and the feed has chips for All / Issues / Changes / Reviews / Comments.

A retired account keeps its page and says so. The page has a Mention button that copies @name.

The visibility rule

Account names are public on this instance; activity is not. Every number and every event is computed over the repositories the reader may open, so an anonymous reader and a maintainer see different totals for the same person, and a private repository contributes nothing to a stranger’s view. The page says “Counted across the repositories you can read” rather than pretending the numbers are total.

An unknown name gets the same 404 every missing page gets. Both pages carry noindex: the names are public, a scraped roster is not what public has to mean.

Where the numbers come from

The events table already holds one row per opening, comment, approval and state change. A profile is a grouped read of those rows (ActorActivity), and the feed is ActorEvents; the directory reads ActorSummary for a page of names in one query. Nothing is counted from the entity tables, so “12 issues opened” cannot drift from the feed, and no profile is written to users.db.

Two supporting changes in forge/index: an events_actor index so a profile is an actor range rather than a scan of everything the viewer can see, and schemaVersion = 11 to rebuild the derived rows. The query-plan test now pins that a profile pages by the actor index.

What deliberately does not happen

  • Git commit authors are not linked and commits are not counted. A commit author is a name somebody typed, not the credential that proves an account, and the index keeps only the top eight authors per default branch. Release events are left out of a profile for the same reason: a tag’s tagger is a Git identity.
  • No keys, passkeys, sessions, or view counters. The instance role is shown to instance admins only, because they can see it on the admin pages already.
  • @name is linked only where CI evaluates mentions — issue and change bodies, comments, release notes. READMEs and documentation files are not linked, so an example in prose does not become a person.

Mention linking

The scanner in metadata is now EachMention, used by both Mentions (the event payload) and the renderer, so what the page links and what wakes a workflow cannot drift apart. The renderer rewrites the parsed tree rather than the markdown source, which keeps inline code, fenced blocks, existing links and backslash escapes intact. Only names that belong to an account become links, so a typo does not produce a 404 somebody can click.

Names elsewhere come from a new user template helper and a shared userlink partial. who is unchanged for the places a link would be wrong: inside another link, a button, an <option>, or a data attribute. Clickable avatars are marked aria-hidden and tabindex="-1" so the name stays the one stop.

Verification

vole workflow run verify passes: format, vet, build, license check, reachability scan, packaging, link checks, and the race test suite.

New tests: profile totals and filters, the anonymous-vs-owner visibility rule on a real two-repository instance, unknown names, retired accounts, directory ordering and search, user resolving only real accounts, and mention linking across emails, URLs, inline code, fenced blocks, escapes, unknown names, and links. Index tests cover the actor reads, the release exclusion, and the query plan.

Checklist

  • vole workflow check
  • vole workflow run verify
  • Docs: docs/accounts-and-access.md, the mention rule in docs/issues.md
  • No new dependency, so licenses.toml and NOTICE are unchanged

Discussion (10)

keenan 7d ago

Switched the avatars to landscapes.

One thing to know first: DiceBear has no landscape style. Its collection is people, robots and abstract marks. So the bundle now carries a Landscape style written against DiceBear’s core API: the seeded PRNG, the option handling, the 100×100 box and the circular mask are still DiceBear’s (MIT), and the style is part of Vole under this project’s licence. The Shapes licence file is gone with Shapes.

The art is one idea:

  • The sky is the account’s ground — the same seven Everforest tones as before, read from the element, so the fallback initial and the finished landscape share a sky.
  • Two or three hills, each the sky a step darker, drawn as circles whose centres sit below the box so only the ridge shows. Back to front with one step of shade between them is what gives depth at 17px without an outline.
  • A sun from Everforest’s cream, yellow or sunset orange, placed above the highest ridge so it is never lost; a low ridge can swallow its foot like a sunset.

The bundle is now 10KB (4KB gzipped) and an avatar is ~470 bytes of SVG, down from 18KB and ~800 because Shapes was the bigger style. Everything else holds: no stored image, no upload, no request to an avatar service, the initial on the tone as the no-script fallback, and a unique clip-mask id per mark.

vole workflow run verify passes, including the licence inventory (one DiceBear licence file now, since the style is ours). I previewed the bundle’s actual output at 20px and 52px on both themes before pushing; at 20px it reads as a sun and a horizon, and the seven skies keep people distinguishable in a list.

keenan 7d ago

Two refinements on top of the landscapes:

  • Rounded squares instead of circles. The element’s border-radius: 30% does the clip, so DiceBear’s circular mask and the per-mark id it carried are gone — each avatar is ~130 bytes smaller and a mark no longer has a document-wide id.
  • The disc follows the theme. On a dark theme it is the moon: pale cream, near-white and a cool grey. On a light theme it stays the sun: cream, yellow and sunset orange. Switching theme in the account page redraws the avatars in place — the bundle watches data-theme on <html>, which theme.js sets, so following the operating system goes through the same path.

Rendered the bundle’s real output at 20px and 52px on both themes with the rounded clip applied before pushing. vole workflow run verify passes.

keenan 7d ago

Fixed the colour system: a mark was one hue in shades of itself, which is far too little variation for an avatar.

  • The sky is still the account’s tone (one of the seven grounds).
  • The hills now take other tones from the same seven, picked from a name-seeded shuffle and never the sky’s own hue. Each is shaded toward the palette’s darkest ground rather than toward black, which darkens it and keeps different hues looking like one landscape.

So every mark has at least two hues, and most have three plus the disc. Your own one, which was clay and four shades of clay, is now clay with an olive hill, a slate hill and a moon.

Colour sets go from 21 per theme to 3,150 — 7 skies × (30 two-hill orders

  • 120 three-hill orders) × 3 discs — before ridge height, radius and disc position vary them further.

To keep one palette rather than two, the seven grounds moved to :root; a tone class now points at one, and the bundle reads all seven from vole.css.

vole workflow run verify passes; previewed the rendered output at 20px and 52px on both themes as before.

keenan 7d ago

The avatar palette now comes from the active Everforest variant instead of fixed hexes.

  • Grounds are each an accent from the palette — green, orange, red, blue, purple, yellow, aqua — mixed toward that variant’s panel colour. The tone class still picks one; the bundle reads all seven from everforest.css.
  • The disc is the theme’s moon (dark) or sun (light) colours, and the ink the hills shade toward is the theme’s darkest token.
  • Two shares of accent, because the panels are opposite: a dark panel is nearly black, so the grounds keep 80% of the accent to remain a colour; a light panel is nearly white, so they keep 48% and stay pastel instead of turning into highlighters.

So a mark now matches whatever variant is configured — dark hard/medium/soft and light hard/medium/soft all give slightly different grounds — and a theme switch redraws it (the palette is cached per theme and re-read when data-theme changes).

One implementation note: a custom property holding color-mix() reads back as text, not as a colour, so the bundle resolves the palette through one hidden probe element rather than parsing CSS. The fallback letter now uses --av-fg (the theme’s foreground), which keeps it readable on the light theme’s pastel grounds.

Previewed the rendered output at 20px and 52px for dark-medium and light-medium; vole workflow run verify passes.

keenan 7d ago

Fixed — that was a real bug, not a palette choice.

getComputedStyle serializes a color-mix() result as color(srgb 0.571765 0.658824 0.461961), and the bundle was reading “the first three numbers” from that string. The digits inside the decimals matched too, so the sky became #00b36b600 — not a colour, which the browser paints black — and every hill was derived from similarly broken input, hence the oversaturation.

The parser now handles both forms a browser can return: rgb()/rgba() for a legacy sRGB value, and color(srgb r g b) with components in 0..1 for anything that came through color-mix(). Components are clamped because color() components are floats.

Verified in headless Chromium against a real instance, before and after, in both themes and at both sizes. The fills are now:

  • dark-medium: sky #bba575, moon #9da9a0, hills [#886759](/vole/n/886759) [#596855](/vole/n/596855) #445a52
  • light-medium: sky #e7c76e, sun #f57d26, hills #b6987a [#889371](/vole/n/889371) #6f8984

So the previous commit’s palette work was right; only the reading of the resolved value was wrong. vole workflow run verify passes.

keenan 7d ago

Four UI updates on top of the avatars:

  • Account menu: the signed-in account’s mark replaces the generic user icon in the top bar.
  • Issues table: a real Author column and a new Assignees column, both showing portraits only. The @name assignee chips moved out of the title cell, so a row is now title + people.
  • Changes table: the author is a portrait rather than a name.
  • Contributors: already rendered through the shared avatar template, so it picked up the new marks with everything else — verified in the browser rather than assumed.

One accessibility detail: in a table cell the portrait is the only content, so there is no printed name beside it. Those cells use a new avatarOnly helper that keeps the profile link and an aria-label with the name, instead of the aria-hidden/tabindex="-1" treatment a decorative avatar beside a printed name gets. A test holds both cases.

Checked against a real instance in headless Chromium: top bar, issue list with two assignees, a change request opened through the form, and the contributor list, at 2× in the dark theme. vole workflow run verify passes.

keenan 7d ago

Review feedback: the marks were harder to read than names, so the avatars are gone.

  • The account menu is back to the user icon.
  • The issue table keeps its author and assignee columns, but with names: the author is a name link, and the assignee cells are the @name chips that link to the filtered list.
  • The change table, the profile header, the directory and the contributor list all show names only.

Everything that existed to draw a mark is deleted, not just hidden: the DiceBear bundle and its licence file, the entry source that built it, the Landscape style, avatar.go and its tests, the avatar template and helpers, the avatar palette in everforest.css, the illustration rules in vole.css, and the app.js block that rendered and re-rendered marks on a theme change. NOTICE is one entry smaller and the licence check confirms the inventory matches what ships.

What stays from this change request is the profile directory, the profile pages, the safe markdown mention links, and the issue/change table layout. vole workflow run verify passes.

keenan 7d ago

Small follow-up: the profile header no longer has the “All people” breadcrumb or the mention-copy button, and their unused CSS is gone.

keenan 7d ago

Small follow-up: the directory is labelled Users instead of People — nav item, page heading and title. The URL is unchanged.

keenan 7d ago

The directory is a table now, and yes — the headers sort it.

  • Columns: user, repositories, issues, changes, reviews, joined. Counts are right-aligned; the active sort column carries aria-sort and an arrow.
  • Ordering: clicking a header orders by it; clicking the active header flips the direction. Counts start largest-first, the name A to Z. It is all in the URL (?sort=reviews&dir=desc), so a view can be linked and it works without JS. If other tables want ordering later, the same URL-driven pattern applies; I would not invent a second mechanism.
  • Filtering: name search plus status chips — everyone, active, no longer active. Retired accounts are marked in their row instead of living in a separate group below the list.
  • Pagination: 50 per page, and the pager keeps the search, the status and the order.

Sorting by a count means the page cannot be cut before the numbers are known, so ActorSummaries now answers for every account with visible work in one grouped query rather than looking up a page of names. That also removes the per-request name list, which is what would have hit SQLite’s bound-variable limit on a large roster.

Checked against an instance with 63 accounts: page 1 shows 50 with a “1–50 of 63” pager, page 2 shows the rest, and ordering by issues puts the only author with work first. vole workflow run verify passes.