Commits
internal/atproto is a minimal client: Login (app password, never the
real account password) via com.atproto.server.createSession, Post via
com.atproto.repo.createRecord (app.bsky.feed.post), truncated to
Bluesky's 300-rune post limit.
A repo pair can now declare an optional bluesky block (handle +
app_password_file, same token-file convention as Forgejo). When set,
GitSyncer and PatchSyncer post a short note whenever they mirror a new
commit or open a patch/PR, alongside the existing activity_log entry.
Absent config is a true no-op everywhere (BlueskyPoster.Post is nil-safe)
— no account configured, no behavior change, verified live: deployed
with zero bluesky config present, service healthy, /social still
correctly shows "not configured" for every series.
Genuinely untestable further without a real account, as flagged in the
v2 plan from the start — the code is complete and defensive (a failed
Bluesky call never blocks or fails the sync loop), but live posting is
unverified until a dedicated Bluesky handle + app password exists to
test against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
RepoSyncer.RadicleDID() resolves the local node identity the same way
GitSyncer already does for remote setup (rad self --nid), and the actor
object now carries it as a PropertyValue attachment — so anyone following
from the fediverse can see, right on the profile, which Radicle node
actually signs this repo's history.
Deliberately scoped down from "AT Proto DID bridging" as discussed: this
surfaces the DID as verifiable profile metadata, not a cross-protocol
identity resolution (that would need a real DID method resolver and a
PDS to test against, neither of which exist here).
Verified live: https://graft.cyberwild.org/actors/federation-x now
carries the correct did:key: attachment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
activity_log now records the forgejo_id/radicle_id of the issue/patch
each entry is about, so a reply's inReplyTo can be resolved back to the
exact item it's a comment on.
Forgejo: CreateIssueComment (POST .../issues/{index}/comments — verified
live, and confirmed the same endpoint takes PR comments too). Radicle:
CommentIssue and CommentPatch via the rad CLI (verified against
`rad issue/patch comment --help` on a live node first, rather than
guessing the syntax — patch comments take a revision id, not the patch's
own id, which the Patch/Revision struct now also carries).
The inbox now handles Create{Note} whose inReplyTo points at one of our
own Notes: strips the reply's HTML, prefixes it with the replying
account's handle, and posts it as a comment on both the Forgejo and
Radicle sides of whichever specific pair logged that entry.
Verified live end-to-end without a real fediverse account, using one of
graft's own already-published actors as the signed sender (its key never
leaves the container — a small Go program run via `pct exec` reads it
locally and signs the request there): the reply landed as a comment on
both f2's issue and the Radicle issue, correctly attributed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
- httpsig.go: draft-cavage HTTP Signatures (sign outbound, verify inbound)
— the fiddliest part of ActivityPub federation, so it ships with a
round-trip test (sign -> receive -> verify) plus a tamper-detection
test, rather than trusting it by inspection.
- Inbox accepts Follow (verifies the signature against the follower's own
published key, records them, replies with a signed Accept) and
Undo{Follow} (removes them). Anything else (replies included) is
accepted and ignored until the comment bridge, M3.
- After each sync pass, DeliverNewActivity signs and POSTs a Create{Note}
to every follower's inbox for whatever landed in activity_log since
that series' last delivery — tracked per-series so a restart never
redelivers history.
- /social now shows each series' live follower count.
Verified live against real malformed/unreachable-actor requests (400 on
bad JSON, 502 on an actor that fails to resolve) with no crash — a real
follow from an actual fediverse account is still the outstanding
end-to-end check, not yet done.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Every series becomes a followable ActivityPub actor
(series@graft.cyberwild.org): WebFinger resolution, an Actor object with
a lazily-generated RSA-2048 keypair, an outbox wrapping activity_log
entries as Create{Note} activities, and per-entry Note objects at stable
URIs (/actors/{series}/notes/{id}) so future replies can address a
specific mirrored commit/issue/patch.
New /social page lists every series' ActivityPub endpoints, linked from
the main dashboard. AT Proto/Bluesky status shows as "not configured"
for now (M5).
Inbox/follow/delivery (M2), the comment bridge (M3), the Radicle DID
profile attachment (M4), and the AT Proto client (M5) are not built yet
— see the v2 plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Previously a series' heatmap only listed sides the activity log happened
to mention, so a Radicle node nothing had been pushed to yet (or a
Forgejo side that only ever received, never originated) was invisible —
looked like only part of the mesh existed. Now every side declared in
config.yaml gets its own row, marked with no events if it hasn't done
anything in the current window yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Dropping the TLD (f1.cyberwild instead of f1.cyberwild.org) saved
little space and just made hosts harder to recognize at a glance.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
repoRootURL only recognized Forgejo's singular /commit/, /issues/,
/pulls/, /patches/ — Radicle Explorer's plural /commits/ never matched,
so every Radicle-hosted commit sub-row silently fell back to a
non-clickable label instead of a link.
hostLabel also only read a URL's host, which for Radicle Explorer is
always the shared frontend domain regardless of which underlying node
served it — the actual node lives in the /nodes/<host>/<rid>/... path.
Two different Radicle nodes mirroring the same repo would have collapsed
into one indistinguishable sub-row; now each gets its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Leftover from testing graft-source's own sync mesh; not meant to live in
the real codebase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Each repo pair now declares an optional series, grouping every side that
mirrors the same underlying repository (its Forgejo instances and Radicle
node) under one row. The dashboard heatmap becomes one event per cell
(never aggregated behind a single tooltip) inside a collapsible group per
repo, expanding to one sub-row per actual mirrored server with its own
link and its own events - instead of one ambiguous shared link.
Also fixes git commit activity always linking to Forgejo regardless of
which side the content actually landed on, so Radicle now shows up as a
real destination instead of being invisible in the activity feed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
- Always light (Atlassian-style), no more automatic dark mode
- Each calendar tooltip entry and each recent commit links directly to
the forge issue/PR/commit or the Radicle explorer page it produced
- Fixed the tooltip being clipped by the calendar's horizontal scroll
container (overflow-y: visible instead of the inherited overflow: auto)
- Footer with the source repository link and license
- activity_log now stores a URL per entry; new optional config fields
source_url (footer link) and radicle.explorer_url (Radicle links)
rad issue open --quiet never prints the created issue's id (only a sync
summary), which silently corrupted the stored radicle_id and caused every
issue to be re-mirrored on each pass. Fixed by dropping --quiet and parsing
the id out of the normal output box instead.
Also adds an append-only activity_log table and a small HTTP dashboard
(GitHub-style activity calendar + recent commits list) served alongside
the existing /healthz and /api/status endpoints.
Reworded sentences for clarity and conciseness in the README.
internal/atproto is a minimal client: Login (app password, never the
real account password) via com.atproto.server.createSession, Post via
com.atproto.repo.createRecord (app.bsky.feed.post), truncated to
Bluesky's 300-rune post limit.
A repo pair can now declare an optional bluesky block (handle +
app_password_file, same token-file convention as Forgejo). When set,
GitSyncer and PatchSyncer post a short note whenever they mirror a new
commit or open a patch/PR, alongside the existing activity_log entry.
Absent config is a true no-op everywhere (BlueskyPoster.Post is nil-safe)
— no account configured, no behavior change, verified live: deployed
with zero bluesky config present, service healthy, /social still
correctly shows "not configured" for every series.
Genuinely untestable further without a real account, as flagged in the
v2 plan from the start — the code is complete and defensive (a failed
Bluesky call never blocks or fails the sync loop), but live posting is
unverified until a dedicated Bluesky handle + app password exists to
test against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
RepoSyncer.RadicleDID() resolves the local node identity the same way
GitSyncer already does for remote setup (rad self --nid), and the actor
object now carries it as a PropertyValue attachment — so anyone following
from the fediverse can see, right on the profile, which Radicle node
actually signs this repo's history.
Deliberately scoped down from "AT Proto DID bridging" as discussed: this
surfaces the DID as verifiable profile metadata, not a cross-protocol
identity resolution (that would need a real DID method resolver and a
PDS to test against, neither of which exist here).
Verified live: https://graft.cyberwild.org/actors/federation-x now
carries the correct did:key: attachment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
activity_log now records the forgejo_id/radicle_id of the issue/patch
each entry is about, so a reply's inReplyTo can be resolved back to the
exact item it's a comment on.
Forgejo: CreateIssueComment (POST .../issues/{index}/comments — verified
live, and confirmed the same endpoint takes PR comments too). Radicle:
CommentIssue and CommentPatch via the rad CLI (verified against
`rad issue/patch comment --help` on a live node first, rather than
guessing the syntax — patch comments take a revision id, not the patch's
own id, which the Patch/Revision struct now also carries).
The inbox now handles Create{Note} whose inReplyTo points at one of our
own Notes: strips the reply's HTML, prefixes it with the replying
account's handle, and posts it as a comment on both the Forgejo and
Radicle sides of whichever specific pair logged that entry.
Verified live end-to-end without a real fediverse account, using one of
graft's own already-published actors as the signed sender (its key never
leaves the container — a small Go program run via `pct exec` reads it
locally and signs the request there): the reply landed as a comment on
both f2's issue and the Radicle issue, correctly attributed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
- httpsig.go: draft-cavage HTTP Signatures (sign outbound, verify inbound)
— the fiddliest part of ActivityPub federation, so it ships with a
round-trip test (sign -> receive -> verify) plus a tamper-detection
test, rather than trusting it by inspection.
- Inbox accepts Follow (verifies the signature against the follower's own
published key, records them, replies with a signed Accept) and
Undo{Follow} (removes them). Anything else (replies included) is
accepted and ignored until the comment bridge, M3.
- After each sync pass, DeliverNewActivity signs and POSTs a Create{Note}
to every follower's inbox for whatever landed in activity_log since
that series' last delivery — tracked per-series so a restart never
redelivers history.
- /social now shows each series' live follower count.
Verified live against real malformed/unreachable-actor requests (400 on
bad JSON, 502 on an actor that fails to resolve) with no crash — a real
follow from an actual fediverse account is still the outstanding
end-to-end check, not yet done.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Every series becomes a followable ActivityPub actor
(series@graft.cyberwild.org): WebFinger resolution, an Actor object with
a lazily-generated RSA-2048 keypair, an outbox wrapping activity_log
entries as Create{Note} activities, and per-entry Note objects at stable
URIs (/actors/{series}/notes/{id}) so future replies can address a
specific mirrored commit/issue/patch.
New /social page lists every series' ActivityPub endpoints, linked from
the main dashboard. AT Proto/Bluesky status shows as "not configured"
for now (M5).
Inbox/follow/delivery (M2), the comment bridge (M3), the Radicle DID
profile attachment (M4), and the AT Proto client (M5) are not built yet
— see the v2 plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Previously a series' heatmap only listed sides the activity log happened
to mention, so a Radicle node nothing had been pushed to yet (or a
Forgejo side that only ever received, never originated) was invisible —
looked like only part of the mesh existed. Now every side declared in
config.yaml gets its own row, marked with no events if it hasn't done
anything in the current window yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
repoRootURL only recognized Forgejo's singular /commit/, /issues/,
/pulls/, /patches/ — Radicle Explorer's plural /commits/ never matched,
so every Radicle-hosted commit sub-row silently fell back to a
non-clickable label instead of a link.
hostLabel also only read a URL's host, which for Radicle Explorer is
always the shared frontend domain regardless of which underlying node
served it — the actual node lives in the /nodes/<host>/<rid>/... path.
Two different Radicle nodes mirroring the same repo would have collapsed
into one indistinguishable sub-row; now each gets its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
Each repo pair now declares an optional series, grouping every side that
mirrors the same underlying repository (its Forgejo instances and Radicle
node) under one row. The dashboard heatmap becomes one event per cell
(never aggregated behind a single tooltip) inside a collapsible group per
repo, expanding to one sub-row per actual mirrored server with its own
link and its own events - instead of one ambiguous shared link.
Also fixes git commit activity always linking to Forgejo regardless of
which side the content actually landed on, so Radicle now shows up as a
real destination instead of being invisible in the activity feed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB7C5CeuMbQujqRtb4foki
- Always light (Atlassian-style), no more automatic dark mode
- Each calendar tooltip entry and each recent commit links directly to
the forge issue/PR/commit or the Radicle explorer page it produced
- Fixed the tooltip being clipped by the calendar's horizontal scroll
container (overflow-y: visible instead of the inherited overflow: auto)
- Footer with the source repository link and license
- activity_log now stores a URL per entry; new optional config fields
source_url (footer link) and radicle.explorer_url (Radicle links)
rad issue open --quiet never prints the created issue's id (only a sync
summary), which silently corrupted the stored radicle_id and caused every
issue to be re-mirrored on each pass. Fixed by dropping --quiet and parsing
the id out of the normal output box instead.
Also adds an append-only activity_log table and a small HTTP dashboard
(GitHub-style activity calendar + recent commits list) served alongside
the existing /healthz and /api/status endpoints.