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.
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.
Self-service federation onboarding + one-time secret sharing
New internal/admin package, wired into the dashboard:
- POST /share/new + GET/POST /share/<id>: a one-time secret share.
AES-256-GCM key derived from the share's own high-entropy id plus a
6-digit passcode, neither of which is ever stored together — only the
id's hash goes in the database, so a full DB dump alone can never
derive the decryption key. Destroyed on first successful claim, after
5 wrong passcodes, or after 15 minutes, whichever comes first. QR code
(pure-Go, github.com/skip2/go-qrcode) encodes the claim link only —
the passcode is still meant to travel over a separate channel.
- GET/POST /add-peer: join an existing federation. Picks up the
series' Radicle side (RID, seed, explorer) automatically — nothing
Radicle-related is ever user-supplied for this flow, since Radicle
already replicates to whatever's seeding that RID.
- GET/POST /new-repo: start a repo graft has never seen, with the
manual prerequisite steps (create the Forgejo repo + token, rad init,
note the RID) spelled out inline before the form.
Both onboarding forms are gated by a single shared admin password
(config.yaml's admin_password, defaulting to "graft" with a startup log
warning when left at that default) checked in constant time, backed by
a rate limiter — 5 failures per IP or 20 globally per 15-minute window,
independent of X-Forwarded-For accuracy. A submitted token is written to
its own chmod-600 file under the same directory static pairs already
use, following the existing token_file convention exactly; only the
path is ever persisted to the database.
Submissions land in a new dynamic_repo table and go live on the very
next sync pass — no restart. cmd/sync/main.go now tracks all runtime
registration (syncers, topology, AI-detection state) behind a mutex,
since it's written from the ticker goroutine and read from HTTP handler
goroutines; each pass checks for newly-approved dynamic repos, builds a
RepoSyncer for each, and rebuilds the dashboard's topology and
Authorized-Integration detection to include them.
CSP's form-action tightened from 'none' to 'self' — the new pages are
plain HTML forms posting back to graft itself; 'none' would have
silently blocked every one of them.
Verified: 9 functional tests (share round-trip, one-time destruction,
max-attempts destruction, wrong-password rejection, rate limiting
blocking even a correct password once tripped, successful add-peer/
new-repo with real DB row + token file content/permissions, QR
generation, clean GET rendering) plus a live production round trip
(share create/claim/QR, wrong-password rejection leaving no trace) and
a Playwright pass confirming zero CSP violations on all three new pages.
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.
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.
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.
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.
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.