Skip to content

Commit 7e0cbc1

Browse files
committed
feat: make activity entries clickable to view on impactindexer.org
- Add rkey column to jetstream_activity table (migration 005) - Include rkey in activity logging for Jetstream, backfill, and population - Add rkey field to GraphQL ActivityEntry type - Update RecentActivity component to link create/update entries to impactindexer.org
1 parent 89a2e2f commit 7e0cbc1

14 files changed

Lines changed: 138 additions & 55 deletions

File tree

client/src/components/dashboard/RecentActivity.tsx

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ interface RecentActivityProps {
88
isLoading?: boolean;
99
}
1010

11+
/**
12+
* Build the URL to view a record on impactindexer.org
13+
*/
14+
function getRecordUrl(entry: ActivityEntry): string | null {
15+
// Only link to create/update operations that have an rkey
16+
if (!entry.rkey || entry.operation === "delete") {
17+
return null;
18+
}
19+
const params = new URLSearchParams({
20+
did: entry.did,
21+
collection: entry.collection,
22+
rkey: entry.rkey,
23+
});
24+
return `https://impactindexer.org/data?${params.toString()}`;
25+
}
26+
1127
export function RecentActivity({ entries, isLoading }: RecentActivityProps) {
1228
return (
1329
<div className="space-y-4">
@@ -31,47 +47,70 @@ export function RecentActivity({ entries, isLoading }: RecentActivityProps) {
3147
</div>
3248
) : (
3349
<div className="divide-y divide-zinc-100">
34-
{entries.slice(0, 10).map((entry) => (
35-
<div
36-
key={entry.id}
37-
className="flex items-center justify-between px-4 py-3"
38-
>
39-
<div className="flex items-center gap-3 min-w-0">
40-
<StatusDot status={entry.status} />
41-
<div className="min-w-0">
42-
<div className="flex items-center gap-2">
43-
<span
44-
className={`px-1.5 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide ${
45-
entry.operation === "create"
46-
? "bg-emerald-50 text-emerald-600"
47-
: entry.operation === "update"
48-
? "bg-blue-50 text-blue-600"
49-
: "bg-amber-50 text-amber-600"
50-
}`}
51-
>
52-
{entry.operation}
53-
</span>
54-
<span className="text-sm font-medium text-zinc-800 truncate">
55-
{entry.collection}
56-
</span>
50+
{entries.slice(0, 10).map((entry) => {
51+
const recordUrl = getRecordUrl(entry);
52+
const content = (
53+
<>
54+
<div className="flex items-center gap-3 min-w-0">
55+
<StatusDot status={entry.status} />
56+
<div className="min-w-0">
57+
<div className="flex items-center gap-2">
58+
<span
59+
className={`px-1.5 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide ${
60+
entry.operation === "create"
61+
? "bg-emerald-50 text-emerald-600"
62+
: entry.operation === "update"
63+
? "bg-blue-50 text-blue-600"
64+
: "bg-amber-50 text-amber-600"
65+
}`}
66+
>
67+
{entry.operation}
68+
</span>
69+
<span className="text-sm font-medium text-zinc-800 truncate">
70+
{entry.collection}
71+
</span>
72+
</div>
73+
<p className="text-xs text-zinc-400 truncate font-mono">
74+
{entry.did.slice(0, 32)}...
75+
</p>
5776
</div>
58-
<p className="text-xs text-zinc-400 truncate font-mono">
59-
{entry.did.slice(0, 32)}...
60-
</p>
6177
</div>
62-
</div>
63-
<div className="text-right shrink-0 ml-4">
64-
<p className="text-xs text-zinc-400 font-mono">
65-
{formatTimestamp(entry.timestamp)}
66-
</p>
67-
{entry.errorMessage && (
68-
<p className="text-xs text-red-500 truncate max-w-[150px]">
69-
{entry.errorMessage}
78+
<div className="text-right shrink-0 ml-4">
79+
<p className="text-xs text-zinc-400 font-mono">
80+
{formatTimestamp(entry.timestamp)}
7081
</p>
71-
)}
82+
{entry.errorMessage && (
83+
<p className="text-xs text-red-500 truncate max-w-[150px]">
84+
{entry.errorMessage}
85+
</p>
86+
)}
87+
</div>
88+
</>
89+
);
90+
91+
if (recordUrl) {
92+
return (
93+
<a
94+
key={entry.id}
95+
href={recordUrl}
96+
target="_blank"
97+
rel="noopener noreferrer"
98+
className="flex items-center justify-between px-4 py-3 hover:bg-zinc-50 transition-colors cursor-pointer"
99+
>
100+
{content}
101+
</a>
102+
);
103+
}
104+
105+
return (
106+
<div
107+
key={entry.id}
108+
className="flex items-center justify-between px-4 py-3"
109+
>
110+
{content}
72111
</div>
73-
</div>
74-
))}
112+
);
113+
})}
75114
</div>
76115
)}
77116
</div>

client/src/lib/graphql/queries.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const GET_RECENT_ACTIVITY = gql`
5757
operation
5858
collection
5959
did
60+
rkey
6061
status
6162
errorMessage
6263
}

client/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ActivityEntry {
3737
operation: string;
3838
collection: string;
3939
did: string;
40+
rkey?: string;
4041
status: string;
4142
errorMessage?: string;
4243
}

cmd/hypergoat/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ func populateActivityFromRecords(
695695
timestamp := extractCreatedAtFromJSON(rec.JSON, rec.IndexedAt)
696696

697697
// Log as a successful create operation
698-
_, err := activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.JSON, "success")
698+
_, err := activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.RKey, rec.JSON, "success")
699699
if err != nil {
700700
return nil // Continue on error
701701
}

hypergoat.png

1.7 MB
Loading

internal/backfill/backfill.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ func (b *Backfiller) processRepo(ctx context.Context, pdsURL string, data *Atpro
539539
if b.activityRepo != nil {
540540
for _, rec := range filteredRecords {
541541
timestamp := extractCreatedAt(rec.JSON)
542-
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.JSON, "success")
542+
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.RKey, rec.JSON, "success")
543543
if err != nil {
544544
slog.Debug("[backfill] Failed to log activity", "uri", rec.URI, "error", err)
545545
}
@@ -668,7 +668,8 @@ func (b *Backfiller) processRepoLegacy(ctx context.Context, pdsURL string, data
668668
// Log activity for the inserted record
669669
if b.activityRepo != nil {
670670
timestamp := extractCreatedAt(string(rec.Value))
671-
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", collection, data.DID, string(rec.Value), "success")
671+
rkey := extractRKeyFromURI(rec.URI)
672+
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", collection, data.DID, rkey, string(rec.Value), "success")
672673
if err != nil {
673674
slog.Debug("[backfill] Failed to log activity", "uri", rec.URI, "error", err)
674675
}
@@ -751,7 +752,7 @@ func (b *Backfiller) BackfillActor(ctx context.Context, did string) (int, error)
751752
if b.activityRepo != nil {
752753
for _, rec := range filteredRecords {
753754
timestamp := extractCreatedAt(rec.JSON)
754-
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.JSON, "success")
755+
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", rec.Collection, rec.DID, rec.RKey, rec.JSON, "success")
755756
if err != nil {
756757
slog.Debug("[backfill] Failed to log activity", "uri", rec.URI, "error", err)
757758
}
@@ -795,7 +796,8 @@ func (b *Backfiller) backfillActorLegacy(ctx context.Context, data *AtprotoData)
795796
// Log activity for the inserted record
796797
if b.activityRepo != nil {
797798
timestamp := extractCreatedAt(string(rec.Value))
798-
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", collection, data.DID, string(rec.Value), "success")
799+
rkey := extractRKeyFromURI(rec.URI)
800+
_, err := b.activityRepo.LogActivityWithStatus(ctx, timestamp, "create", collection, data.DID, rkey, string(rec.Value), "success")
799801
if err != nil {
800802
slog.Debug("[backfill] Failed to log activity", "uri", rec.URI, "error", err)
801803
}
@@ -829,6 +831,16 @@ func ParseCollections(s string) []string {
829831
return result
830832
}
831833

834+
// extractRKeyFromURI extracts the rkey from an AT-URI (at://did/collection/rkey).
835+
func extractRKeyFromURI(uri string) string {
836+
// URI format: at://did/collection/rkey
837+
parts := strings.Split(uri, "/")
838+
if len(parts) >= 5 {
839+
return parts[len(parts)-1]
840+
}
841+
return ""
842+
}
843+
832844
// extractCreatedAt extracts the createdAt timestamp from a record's JSON.
833845
// Returns the parsed time or the current time if not found/parseable.
834846
func extractCreatedAt(recordJSON string) time.Time {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP INDEX IF EXISTS idx_jetstream_activity_rkey;
2+
ALTER TABLE jetstream_activity DROP COLUMN rkey;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Add rkey column to jetstream_activity for linking to records
2+
ALTER TABLE jetstream_activity ADD COLUMN rkey TEXT;
3+
4+
-- Add index for efficient lookups by rkey
5+
CREATE INDEX IF NOT EXISTS idx_jetstream_activity_rkey ON jetstream_activity(rkey);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Note: SQLite doesn't support DROP COLUMN directly in older versions
2+
-- This is a no-op for rollback; manual intervention may be needed
3+
-- DROP INDEX IF EXISTS idx_jetstream_activity_rkey;
4+
-- ALTER TABLE jetstream_activity DROP COLUMN rkey;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Add rkey column to jetstream_activity for linking to records
2+
ALTER TABLE jetstream_activity ADD COLUMN rkey TEXT;
3+
4+
-- Add index for efficient lookups by rkey
5+
CREATE INDEX IF NOT EXISTS idx_jetstream_activity_rkey ON jetstream_activity(rkey);

0 commit comments

Comments
 (0)