Simplify dashboard to kind-only colors; add comment sync from 4 origins
Dashboard: removed the separate "source"/"replicated" cell colors and
legend entries. Cells now always color by event kind (commit, issue,
patch, comment) — the fact that a side holds content without a
graft-logged push of its own (Radicle gossip, an Authorized Integration,
or simply being the origin) is now a tooltip note only, never a visual
category of its own.
New "comment" kind, tracked from four origins, each recorded in a new
activity_log.origin column and shown as a tooltip note (never a color):
- Forgejo <-> Radicle, native: internal/sync/comments.go polls both
sides' comments each pass and cross-mirrors new ones, prefixed
"**via Forgejo:**" / "**via Radicle:**". Dedup via a new comment_seen
table (content hash, scoped per repo_pair+kind+item) plus a prefix
guard against re-mirroring graft's own output — verified live: a
native Forgejo comment mirrored to Radicl<e exactly once, confirmed
stable (no duplicate) across multiple subsequent sync passes.
- ActivityPub: handleReply now logs its existing comment bridge as a
dashboard activity (previously invisible on the dashboard).
- AT Proto: new inbound reply polling (pollBlueskyReplies in
cmd/sync/bluesky_replies.go), using app.bsky.notification.listNotifications.
NOT verified against a live account — none is configured in this
deployment; built directly from AT Proto's published lexicon. Required
extending atproto.Client.Post and sync.BlueskyPoster.Post to return
the new post's AT-URI (new atproto_post table) so a later reply's
parent can be traced back to the activity it was about, and
state.Store.LogActivity to return the new row's id.
Bug caught by a unit test before deploy: the three new comment/reply
truncation helpers sliced on byte index, which both overshot the byte
bound (a 3-byte "…" past a 200-byte cutoff) and could cut a multi-byte
UTF-8 rune in half — fixed to slice on runes, matching the pattern
atproto.Client.Post already used correctly.
Add Radicle peer onboarding; fix dynamic peers vanishing on restart
New /add-radicle-peer: connects a new Radicle node to the mesh and
seeds an existing federation's RID (rad node connect + rad seed on
graft's own node), gated by the same admin password + rate limiter as
the Forgejo onboarding forms. No DB row needed — it's a one-time
operational action, not an ongoing sync pair. Cross-linked with
/add-peer; dashboard gets a third button.
Fixes a real bug caught live: federation-x-f3 (added via /add-peer
earlier) silently dropped out of the sync loop on every graft restart
since then — main() only rebuilt live RepoSyncers for dynamic_repo rows
still unmaterialized, never for ones a previous run had already
materialized, so a dynamic peer's syncer simply ceased to exist after
any deploy. Confirmed via f3's README staying stale for ~50 minutes
across several restarts while f2/bimr updated normally. Fixed with
loadAllDynamicRepos, called once at startup regardless of the
materialized flag — deliberately non-fatal on a load failure (unlike a
bad static config.yaml pair), since the self-service path exists
specifically to tolerate transient failures, not turn one peer's
hiccup into a full outage. Verified live: f3 reloaded on restart,
caught up on a real README change within one sync pass.
Also: dropped three redundant/stale sentences from the onboarding page
copy (a leftover "not entered here" clause, a since-superseded
auto-init warning now covered by the Adding-a-peer docs instead, and a
"no restart" hint repeated between the form and its own success page),
and "Pair name" -> "Peer name" for consistency with the rest of the UI.