Highly ambitious ATProtocol AppView service and sdks

fix get all slices query to properly resolve all slices, clean up jetstream ui

Changed files
+107 -240
api
frontend
+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
··· 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
··· 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
··· 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
··· 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
+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
··· 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
··· 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
··· 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">