+4
-5
api/src/database/slices.rs
+4
-5
api/src/database/slices.rs
···
147
147
Ok(count.count.unwrap_or(0))
148
148
}
149
149
150
-
/// Gets all slice URIs that have lexicons defined.
150
+
/// Gets all slice URIs from network.slices.slice records.
151
151
///
152
-
/// Useful for discovering all active slices in the system.
152
+
/// Returns all slices that exist in the system
153
153
pub async fn get_all_slices(&self) -> Result<Vec<String>, DatabaseError> {
154
154
let rows: Vec<(String,)> = sqlx::query_as(
155
155
r#"
156
-
SELECT DISTINCT json->>'slice' as slice_uri
156
+
SELECT DISTINCT uri as slice_uri
157
157
FROM record
158
-
WHERE collection = 'network.slices.lexicon'
159
-
AND json->>'slice' IS NOT NULL
158
+
WHERE collection = 'network.slices.slice'
160
159
"#,
161
160
)
162
161
.fetch_all(&self.pool)
+43
-15
api/src/jetstream.rs
+43
-15
api/src/jetstream.rs
···
453
453
} else {
454
454
format!("Record updated in {}", commit.collection)
455
455
};
456
-
let operation = if is_insert { "insert" } else { "update" };
457
456
Logger::global().log_jetstream_with_slice(
458
457
LogLevel::Info,
459
458
&message,
460
459
Some(serde_json::json!({
461
-
"operation": operation,
462
-
"collection": commit.collection,
463
-
"slice_uri": slice_uri,
464
460
"did": did,
461
+
"kind": "commit",
462
+
"commit": {
463
+
"rev": commit.rev,
464
+
"operation": commit.operation,
465
+
"collection": commit.collection,
466
+
"rkey": commit.rkey,
467
+
"record": commit.record,
468
+
"cid": commit.cid
469
+
},
470
+
"indexed_operation": if is_insert { "insert" } else { "update" },
465
471
"record_type": "primary"
466
472
})),
467
473
Some(&slice_uri),
···
473
479
LogLevel::Error,
474
480
message,
475
481
Some(serde_json::json!({
476
-
"operation": "upsert",
477
-
"collection": commit.collection,
478
-
"slice_uri": slice_uri,
479
482
"did": did,
483
+
"kind": "commit",
484
+
"commit": {
485
+
"rev": commit.rev,
486
+
"operation": commit.operation,
487
+
"collection": commit.collection,
488
+
"rkey": commit.rkey,
489
+
"record": commit.record,
490
+
"cid": commit.cid
491
+
},
480
492
"error": e.to_string(),
481
493
"record_type": "primary"
482
494
})),
···
517
529
} else {
518
530
format!("Record updated in {}", commit.collection)
519
531
};
520
-
let operation = if is_insert { "insert" } else { "update" };
521
532
Logger::global().log_jetstream_with_slice(
522
533
LogLevel::Info,
523
534
&message,
524
535
Some(serde_json::json!({
525
-
"operation": operation,
526
-
"collection": commit.collection,
527
-
"slice_uri": slice_uri,
528
536
"did": did,
537
+
"kind": "commit",
538
+
"commit": {
539
+
"rev": commit.rev,
540
+
"operation": commit.operation,
541
+
"collection": commit.collection,
542
+
"rkey": commit.rkey,
543
+
"record": commit.record,
544
+
"cid": commit.cid
545
+
},
546
+
"indexed_operation": if is_insert { "insert" } else { "update" },
529
547
"record_type": "external"
530
548
})),
531
549
Some(&slice_uri),
···
537
555
LogLevel::Error,
538
556
message,
539
557
Some(serde_json::json!({
540
-
"operation": "upsert",
541
-
"collection": commit.collection,
542
-
"slice_uri": slice_uri,
543
558
"did": did,
559
+
"kind": "commit",
560
+
"commit": {
561
+
"rev": commit.rev,
562
+
"operation": commit.operation,
563
+
"collection": commit.collection,
564
+
"rkey": commit.rkey,
565
+
"record": commit.record,
566
+
"cid": commit.cid
567
+
},
544
568
"error": e.to_string(),
545
569
"record_type": "external"
546
570
})),
···
623
647
}
624
648
625
649
// Handle cascade deletion before deleting the record
626
-
if let Err(e) = self.database.handle_cascade_deletion(&uri, &commit.collection).await {
650
+
if let Err(e) = self
651
+
.database
652
+
.handle_cascade_deletion(&uri, &commit.collection)
653
+
.await
654
+
{
627
655
warn!("Cascade deletion failed for {}: {}", uri, e);
628
656
}
629
657
+2
-10
api/src/logging.rs
+2
-10
api/src/logging.rs
···
460
460
let limit = limit.unwrap_or(100);
461
461
462
462
let rows = if let Some(slice_uri) = slice_filter {
463
-
tracing::info!("Querying jetstream logs with slice filter: {}", slice_uri);
464
463
// Include both slice-specific logs and global connection logs for context
465
-
let results = sqlx::query_as!(
464
+
sqlx::query_as!(
466
465
LogEntry,
467
466
r#"
468
467
SELECT id, created_at, log_type, job_id, user_did, slice_uri, level, message, metadata
···
476
475
limit
477
476
)
478
477
.fetch_all(pool)
479
-
.await?;
480
-
481
-
tracing::info!(
482
-
"Found {} jetstream logs for slice {}",
483
-
results.len(),
484
-
slice_uri
485
-
);
486
-
results
478
+
.await?
487
479
} else {
488
480
// No filter provided, return all Jetstream logs across all slices
489
481
sqlx::query_as!(
+12
-130
frontend/src/features/slices/jetstream/handlers.tsx
+12
-130
frontend/src/features/slices/jetstream/handlers.tsx
···
1
1
import type { Route } from "@std/http/unstable-route";
2
-
import { requireAuth, withAuth } from "../../../routes/middleware.ts";
2
+
import { withAuth } from "../../../routes/middleware.ts";
3
3
import {
4
4
requireSliceAccess,
5
5
withSliceAccess,
···
7
7
import { getSliceClient } from "../../../utils/client.ts";
8
8
import { publicClient } from "../../../config.ts";
9
9
import { renderHTML } from "../../../utils/render.tsx";
10
-
import { Layout } from "../../../shared/fragments/Layout.tsx";
11
10
import { extractSliceParams } from "../../../utils/slice-params.ts";
12
11
import { JetstreamLogsPage } from "./templates/JetstreamLogsPage.tsx";
13
-
import { JetstreamLogs } from "./templates/fragments/JetstreamLogs.tsx";
14
-
import { JetstreamStatus } from "./templates/fragments/JetstreamStatus.tsx";
15
-
import { JetstreamStatusDisplay } from "./templates/fragments/JetstreamStatusDisplay.tsx";
16
12
import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts";
17
-
import { buildSliceUri } from "../../../utils/at-uri.ts";
18
-
19
-
async function handleJetstreamLogs(
20
-
req: Request,
21
-
params?: URLPatternResult
22
-
): Promise<Response> {
23
-
const context = await withAuth(req);
24
-
const authResponse = requireAuth(context);
25
-
if (authResponse) return authResponse;
26
-
27
-
const sliceId = params?.pathname.groups.id;
28
-
if (!sliceId) {
29
-
return renderHTML(
30
-
<div className="p-8 text-center text-red-600">❌ Invalid slice ID</div>,
31
-
{ status: 400 }
32
-
);
33
-
}
34
-
35
-
try {
36
-
// Use the slice-specific client
37
-
const sliceClient = getSliceClient(context, sliceId);
38
-
39
-
// Build slice URI from the user's DID and sliceId
40
-
const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
41
-
42
-
// Get Jetstream logs
43
-
const result = await sliceClient.network.slices.slice.getJetstreamLogs({
44
-
slice: sliceUri,
45
-
limit: 100,
46
-
});
47
-
48
-
const logs = result?.logs || [];
49
-
50
-
// Sort logs in descending order (newest first)
51
-
const sortedLogs = logs.sort(
52
-
(a, b) =>
53
-
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
54
-
);
55
-
56
-
// Render the log content
57
-
return renderHTML(<JetstreamLogs logs={sortedLogs} />);
58
-
} catch (error) {
59
-
console.error("Failed to get Jetstream logs:", error);
60
-
const errorMessage = error instanceof Error ? error.message : String(error);
61
-
return renderHTML(
62
-
<Layout title="Error">
63
-
<div className="max-w-6xl mx-auto">
64
-
<div className="flex items-center gap-4 mb-6">
65
-
<a
66
-
href={`/profile/${context.currentUser.handle}/slice/${sliceId}`}
67
-
className="text-blue-600 hover:text-blue-800"
68
-
>
69
-
← Back to Slice
70
-
</a>
71
-
<h1 className="text-2xl font-semibold text-gray-900">
72
-
✈️ Jetstream Logs
73
-
</h1>
74
-
</div>
75
-
<div className="p-8 text-center text-red-600">
76
-
❌ Error loading Jetstream logs: {errorMessage}
77
-
</div>
78
-
</div>
79
-
</Layout>,
80
-
{ status: 500 }
81
-
);
82
-
}
83
-
}
84
-
85
-
async function handleJetstreamStatus(
86
-
req: Request,
87
-
_params?: URLPatternResult
88
-
): Promise<Response> {
89
-
try {
90
-
// Extract parameters from query
91
-
const url = new URL(req.url);
92
-
const isCompact = url.searchParams.get("compact") === "true";
93
-
const sliceId = url.searchParams.get("sliceId") || undefined;
94
-
const handle = url.searchParams.get("handle") || undefined;
95
-
96
-
// Fetch jetstream status using the public client
97
-
const data = await publicClient.network.slices.slice.getJetstreamStatus();
98
-
99
-
// Render compact version for logs page
100
-
if (isCompact) {
101
-
return renderHTML(
102
-
<JetstreamStatusDisplay connected={data.connected} isCompact />
103
-
);
104
-
}
105
-
106
-
// Render full version for main page
107
-
return renderHTML(
108
-
<JetstreamStatus
109
-
connected={data.connected}
110
-
sliceId={sliceId}
111
-
handle={handle}
112
-
/>
113
-
);
114
-
} catch (_error) {
115
-
// Extract parameters for error case too
116
-
const url = new URL(req.url);
117
-
const isCompact = url.searchParams.get("compact") === "true";
118
-
const sliceId = url.searchParams.get("sliceId") || undefined;
119
-
const handle = url.searchParams.get("handle") || undefined;
120
-
121
-
// Render compact error version
122
-
if (isCompact) {
123
-
return renderHTML(<JetstreamStatusDisplay connected={false} isCompact />);
124
-
}
125
-
126
-
// Fallback to disconnected state on error for full version
127
-
return renderHTML(
128
-
<JetstreamStatus connected={false} sliceId={sliceId} handle={handle} />
129
-
);
130
-
}
131
-
}
132
13
133
14
async function handleJetstreamLogsPage(
134
15
req: Request,
···
167
48
console.error("Failed to fetch Jetstream logs:", error);
168
49
}
169
50
51
+
// Fetch jetstream status
52
+
let jetstreamConnected = false;
53
+
try {
54
+
const jetstreamStatus =
55
+
await publicClient.network.slices.slice.getJetstreamStatus();
56
+
jetstreamConnected = jetstreamStatus.connected;
57
+
} catch (error) {
58
+
console.error("Failed to fetch Jetstream status:", error);
59
+
}
60
+
170
61
return renderHTML(
171
62
<JetstreamLogsPage
172
63
slice={context.sliceContext!.slice!}
173
64
logs={logs}
174
65
sliceId={sliceParams.sliceId}
175
66
currentUser={authContext.currentUser}
67
+
jetstreamConnected={jetstreamConnected}
176
68
/>
177
69
);
178
70
}
···
184
76
pathname: "/profile/:handle/slice/:rkey/jetstream",
185
77
}),
186
78
handler: handleJetstreamLogsPage,
187
-
},
188
-
{
189
-
method: "GET",
190
-
pattern: new URLPattern({ pathname: "/api/jetstream/status" }),
191
-
handler: handleJetstreamStatus,
192
-
},
193
-
{
194
-
method: "GET",
195
-
pattern: new URLPattern({ pathname: "/api/slices/:id/jetstream/logs" }),
196
-
handler: handleJetstreamLogs,
197
79
},
198
80
];
+6
-9
frontend/src/features/slices/jetstream/templates/JetstreamLogsPage.tsx
+6
-9
frontend/src/features/slices/jetstream/templates/JetstreamLogsPage.tsx
···
13
13
logs: NetworkSlicesSliceGetJobLogsLogEntry[];
14
14
sliceId: string;
15
15
currentUser?: AuthenticatedUser;
16
+
jetstreamConnected?: boolean;
16
17
}
17
18
18
19
export function JetstreamLogsPage({
···
20
21
logs,
21
22
sliceId,
22
23
currentUser,
24
+
jetstreamConnected = false,
23
25
}: JetstreamLogsPageProps) {
26
+
const sliceUrl = buildSliceUrlFromView(slice, sliceId);
24
27
return (
25
28
<SliceLogPage
26
29
slice={slice}
···
28
31
currentUser={currentUser}
29
32
title="Jetstream Logs"
30
33
breadcrumbItems={[
31
-
{ label: slice.name, href: buildSliceUrlFromView(slice, sliceId) },
34
+
{ label: slice.name, href: sliceUrl },
32
35
{ label: "Jetstream Logs" },
33
36
]}
34
-
headerActions={<JetstreamStatusCompact sliceId={sliceId} />}
37
+
headerActions={<JetstreamStatusCompact connected={jetstreamConnected} />}
35
38
>
36
-
<div
37
-
hx-get={`/api/slices/${sliceId}/jetstream/logs`}
38
-
hx-trigger="load, every 20s"
39
-
hx-swap="innerHTML"
40
-
>
41
-
<JetstreamLogs logs={logs} />
42
-
</div>
39
+
<JetstreamLogs logs={logs} />
43
40
</SliceLogPage>
44
41
);
45
42
}
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
frontend/src/features/slices/overview/templates/fragments/JetstreamStatus.tsx
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatus.tsx
frontend/src/features/slices/overview/templates/fragments/JetstreamStatus.tsx
+21
-9
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusCompact.tsx
+21
-9
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusCompact.tsx
···
1
1
import { Text } from "../../../../../shared/fragments/Text.tsx";
2
2
3
-
export function JetstreamStatusCompact({ sliceId }: { sliceId: string }) {
3
+
interface JetstreamStatusCompactProps {
4
+
connected: boolean;
5
+
}
6
+
7
+
export function JetstreamStatusCompact({ connected }: JetstreamStatusCompactProps) {
4
8
return (
5
-
<div
6
-
hx-get={`/api/jetstream/status?sliceId=${sliceId}&compact=true`}
7
-
hx-trigger="load, every 2m"
8
-
hx-swap="outerHTML"
9
-
className="inline-flex items-center gap-2 text-xs"
10
-
>
11
-
<div className="w-2 h-2 bg-zinc-400 dark:bg-zinc-500 rounded-full"></div>
12
-
<Text as="span" variant="muted" size="xs">Checking status...</Text>
9
+
<div className="inline-flex items-center gap-2 text-xs">
10
+
{connected ? (
11
+
<>
12
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
13
+
<Text as="span" variant="success" size="xs">
14
+
Jetstream Connected
15
+
</Text>
16
+
</>
17
+
) : (
18
+
<>
19
+
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
20
+
<Text as="span" variant="error" size="xs">
21
+
Jetstream Offline
22
+
</Text>
23
+
</>
24
+
)}
13
25
</div>
14
26
);
15
27
}
-33
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusDisplay.tsx
-33
frontend/src/features/slices/jetstream/templates/fragments/JetstreamStatusDisplay.tsx
···
1
-
import { Text } from "../../../../../shared/fragments/Text.tsx";
2
-
3
-
interface JetstreamStatusDisplayProps {
4
-
connected: boolean;
5
-
isCompact?: boolean;
6
-
}
7
-
8
-
export function JetstreamStatusDisplay({ connected, isCompact = false }: JetstreamStatusDisplayProps) {
9
-
if (isCompact) {
10
-
return (
11
-
<div className="inline-flex items-center gap-2 text-xs">
12
-
{connected ? (
13
-
<>
14
-
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
15
-
<Text as="span" variant="success" size="xs">
16
-
Jetstream Connected
17
-
</Text>
18
-
</>
19
-
) : (
20
-
<>
21
-
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
22
-
<Text as="span" variant="error" size="xs">
23
-
Jetstream Offline
24
-
</Text>
25
-
</>
26
-
)}
27
-
</div>
28
-
);
29
-
}
30
-
31
-
// Full version would be handled by the existing JetstreamStatus component
32
-
return null;
33
-
}
+11
frontend/src/features/slices/overview/handlers.tsx
+11
frontend/src/features/slices/overview/handlers.tsx
···
7
7
withSliceAccess,
8
8
} from "../../../routes/slice-middleware.ts";
9
9
import { extractSliceParams } from "../../../utils/slice-params.ts";
10
+
import { publicClient } from "../../../config.ts";
10
11
11
12
async function handleSliceOverview(
12
13
req: Request,
···
44
45
actors: stat.actors,
45
46
}));
46
47
48
+
// Fetch jetstream status
49
+
let jetstreamConnected = false;
50
+
try {
51
+
const jetstreamStatus = await publicClient.network.slices.slice.getJetstreamStatus();
52
+
jetstreamConnected = jetstreamStatus.connected;
53
+
} catch (error) {
54
+
console.error("Failed to fetch Jetstream status:", error);
55
+
}
56
+
47
57
return renderHTML(
48
58
<SliceOverview
49
59
slice={context.sliceContext!.slice!}
···
52
62
currentTab="overview"
53
63
currentUser={authContext.currentUser}
54
64
hasSliceAccess={context.sliceContext?.hasAccess}
65
+
jetstreamConnected={jetstreamConnected}
55
66
/>,
56
67
);
57
68
}
+8
-29
frontend/src/features/slices/overview/templates/SliceOverview.tsx
+8
-29
frontend/src/features/slices/overview/templates/SliceOverview.tsx
···
6
6
import { Text } from "../../../../shared/fragments/Text.tsx";
7
7
import { Link } from "../../../../shared/fragments/Link.tsx";
8
8
import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts";
9
+
import { JetstreamStatus } from "./fragments/JetstreamStatus.tsx";
9
10
10
11
function formatNumber(num: number): string {
11
12
return num.toLocaleString();
···
24
25
currentTab?: string;
25
26
currentUser?: AuthenticatedUser;
26
27
hasSliceAccess?: boolean;
28
+
jetstreamConnected?: boolean;
27
29
}
28
30
29
31
export function SliceOverview({
···
33
35
currentTab = "overview",
34
36
currentUser,
35
37
hasSliceAccess,
38
+
jetstreamConnected = false,
36
39
}: SliceOverviewProps) {
37
40
return (
38
41
<SlicePage
···
42
45
currentUser={currentUser}
43
46
hasSliceAccess={hasSliceAccess}
44
47
>
45
-
<div
46
-
hx-get={`/api/jetstream/status?sliceId=${sliceId}&handle=${slice.creator?.handle}`}
47
-
hx-trigger="load, every 2m"
48
-
hx-swap="outerHTML"
49
-
>
50
-
<Card padding="sm" className="mb-6">
51
-
<div className="flex items-center justify-between">
52
-
<div className="flex items-center">
53
-
<div className="w-3 h-3 bg-zinc-400 dark:bg-zinc-500 rounded-full mr-3"></div>
54
-
<div>
55
-
<Text
56
-
as="h3"
57
-
size="sm"
58
-
variant="secondary"
59
-
className="font-semibold block"
60
-
>
61
-
🌊 Checking Jetstream Status...
62
-
</Text>
63
-
<Text as="p" size="xs" variant="muted">
64
-
Loading connection status
65
-
</Text>
66
-
</div>
67
-
</div>
68
-
<Text as="span" size="xs" variant="muted">
69
-
Checking...
70
-
</Text>
71
-
</div>
72
-
</Card>
73
-
</div>
48
+
<JetstreamStatus
49
+
connected={jetstreamConnected}
50
+
sliceId={sliceId}
51
+
handle={slice.creator?.handle}
52
+
/>
74
53
75
54
{(slice.indexedRecordCount ?? 0) > 0 && (
76
55
<Card padding="md" className="mb-8">