Highly ambitious ATProtocol AppView service and sdks

fix casing inconsistencies

+101 -99
+14 -14
api/scripts/generate-typescript.ts
··· 139 139 { name: "did", type: "string" }, 140 140 { name: "collection", type: "string" }, 141 141 { name: "value", type: "T" }, 142 - { name: "indexed_at", type: "string" }, 142 + { name: "indexedAt", type: "string" }, 143 143 ], 144 144 }); 145 145 ··· 194 194 { name: "did", type: "string" }, 195 195 { name: "collection", type: "string" }, 196 196 { name: "value", type: "Record<string, unknown>" }, 197 - { name: "indexed_at", type: "string" }, 197 + { name: "indexedAt", type: "string" }, 198 198 ], 199 199 }); 200 200 ··· 213 213 isExported: true, 214 214 properties: [ 215 215 { name: "success", type: "boolean" }, 216 - { name: "generated_code", type: "string", hasQuestionToken: true }, 216 + { name: "generatedCode", type: "string", hasQuestionToken: true }, 217 217 { name: "error", type: "string", hasQuestionToken: true }, 218 218 ], 219 219 }); ··· 224 224 isExported: true, 225 225 properties: [ 226 226 { name: "collections", type: "string[]", hasQuestionToken: true }, 227 - { name: "external_collections", type: "string[]", hasQuestionToken: true }, 227 + { name: "externalCollections", type: "string[]", hasQuestionToken: true }, 228 228 { name: "repos", type: "string[]", hasQuestionToken: true }, 229 - { name: "limit_per_repo", type: "number", hasQuestionToken: true }, 229 + { name: "limitPerRepo", type: "number", hasQuestionToken: true }, 230 230 ], 231 231 }); 232 232 ··· 235 235 isExported: true, 236 236 properties: [ 237 237 { name: "success", type: "boolean" }, 238 - { name: "total_records", type: "number" }, 239 - { name: "collections_synced", type: "string[]" }, 240 - { name: "repos_processed", type: "number" }, 238 + { name: "totalRecords", type: "number" }, 239 + { name: "collectionsSynced", type: "string[]" }, 240 + { name: "reposProcessed", type: "number" }, 241 241 { name: "message", type: "string" }, 242 242 ], 243 243 }); ··· 247 247 isExported: true, 248 248 properties: [ 249 249 { name: "collection", type: "string" }, 250 - { name: "record_count", type: "number" }, 251 - { name: "unique_actors", type: "number" }, 250 + { name: "recordCount", type: "number" }, 251 + { name: "uniqueActors", type: "number" }, 252 252 ], 253 253 }); 254 254 ··· 266 266 properties: [ 267 267 { name: "success", type: "boolean" }, 268 268 { name: "collections", type: "string[]" }, 269 - { name: "collection_stats", type: "CollectionStats[]" }, 270 - { name: "total_lexicons", type: "number" }, 271 - { name: "total_records", type: "number" }, 272 - { name: "total_actors", type: "number" }, 269 + { name: "collectionStats", type: "CollectionStats[]" }, 270 + { name: "totalLexicons", type: "number" }, 271 + { name: "totalRecords", type: "number" }, 272 + { name: "totalActors", type: "number" }, 273 273 { name: "message", type: "string", hasQuestionToken: true }, 274 274 ], 275 275 });
+11 -3
api/src/models.rs
··· 3 3 use serde_json::Value; 4 4 5 5 #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 6 + #[serde(rename_all = "camelCase")] 6 7 pub struct Record { 7 8 pub uri: String, 8 9 pub cid: String, 9 10 pub did: String, 10 11 pub collection: String, 11 12 pub json: Value, 12 - #[serde(rename = "indexedAt")] 13 13 pub indexed_at: DateTime<Utc>, 14 14 } 15 15 16 16 #[derive(Debug, Serialize, Deserialize)] 17 + #[serde(rename_all = "camelCase")] 17 18 pub struct IndexedRecord { 18 19 pub uri: String, 19 20 pub cid: String, 20 21 pub did: String, 21 22 pub collection: String, 22 23 pub value: Value, 23 - #[serde(rename = "indexedAt")] 24 24 pub indexed_at: String, 25 25 } 26 26 27 27 #[derive(Debug, Serialize, Deserialize)] 28 + #[serde(rename_all = "camelCase")] 28 29 pub struct ListRecordsOutput { 29 30 pub records: Vec<IndexedRecord>, 30 31 pub cursor: Option<String>, 31 32 } 32 33 33 34 #[derive(Debug, Serialize, Deserialize)] 35 + #[serde(rename_all = "camelCase")] 34 36 pub struct BulkSyncParams { 35 37 pub collections: Option<Vec<String>>, 36 38 pub external_collections: Option<Vec<String>>, ··· 39 41 } 40 42 41 43 #[derive(Debug, Serialize, Deserialize)] 44 + #[serde(rename_all = "camelCase")] 42 45 pub struct BulkSyncOutput { 43 46 pub success: bool, 44 47 pub total_records: i64, ··· 48 51 } 49 52 50 53 #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 54 + #[serde(rename_all = "camelCase")] 51 55 pub struct Actor { 52 56 pub did: String, 53 57 pub handle: Option<String>, 54 - #[serde(rename = "indexedAt")] 55 58 pub indexed_at: String, 56 59 } 57 60 58 61 #[derive(Debug, Serialize, Deserialize)] 62 + #[serde(rename_all = "camelCase")] 59 63 pub struct CollectionStats { 60 64 pub collection: String, 61 65 pub record_count: i64, ··· 63 67 } 64 68 65 69 #[derive(Debug, Serialize, Deserialize)] 70 + #[serde(rename_all = "camelCase")] 66 71 pub struct SliceStatsParams { 67 72 pub slice: String, 68 73 } 69 74 70 75 #[derive(Debug, Serialize, Deserialize)] 76 + #[serde(rename_all = "camelCase")] 71 77 pub struct SliceStatsOutput { 72 78 pub success: bool, 73 79 pub collections: Vec<String>, ··· 79 85 } 80 86 81 87 #[derive(Debug, Serialize, Deserialize)] 88 + #[serde(rename_all = "camelCase")] 82 89 pub struct SliceRecordsParams { 83 90 pub slice: String, 84 91 pub collection: String, ··· 88 95 } 89 96 90 97 #[derive(Debug, Serialize, Deserialize)] 98 + #[serde(rename_all = "camelCase")] 91 99 pub struct SliceRecordsOutput { 92 100 pub success: bool, 93 101 pub records: Vec<IndexedRecord>,
+58 -64
frontend/src/client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-08-24 22:36:01 UTC 2 + // Generated at: 2025-08-26 15:32:09 UTC 3 3 // Lexicons: 3 4 4 5 5 /** ··· 33 33 did: string; 34 34 collection: string; 35 35 value: T; 36 - indexed_at: string; 36 + indexedAt: string; 37 37 } 38 38 39 39 export interface ListRecordsResponse<T> { ··· 64 64 did: string; 65 65 collection: string; 66 66 value: Record<string, unknown>; 67 - indexed_at: string; 67 + indexedAt: string; 68 68 } 69 69 70 70 export interface CodegenXrpcRequest { ··· 74 74 75 75 export interface CodegenXrpcResponse { 76 76 success: boolean; 77 - generated_code?: string; 77 + generatedCode?: string; 78 78 error?: string; 79 79 } 80 80 81 81 export interface BulkSyncParams { 82 82 collections?: string[]; 83 - external_collections?: string[]; 83 + externalCollections?: string[]; 84 84 repos?: string[]; 85 - limit_per_repo?: number; 85 + limitPerRepo?: number; 86 86 } 87 87 88 88 export interface BulkSyncOutput { 89 89 success: boolean; 90 - total_records: number; 91 - collections_synced: string[]; 92 - repos_processed: number; 90 + totalRecords: number; 91 + collectionsSynced: string[]; 92 + reposProcessed: number; 93 93 message: string; 94 94 } 95 95 96 96 export interface CollectionStats { 97 97 collection: string; 98 - record_count: number; 99 - unique_actors: number; 98 + recordCount: number; 99 + uniqueActors: number; 100 100 } 101 101 102 102 export interface SliceStatsParams { ··· 106 106 export interface SliceStatsOutput { 107 107 success: boolean; 108 108 collections: string[]; 109 - collection_stats: CollectionStats[]; 110 - total_lexicons: number; 111 - total_records: number; 112 - total_actors: number; 109 + collectionStats: CollectionStats[]; 110 + totalLexicons: number; 111 + totalRecords: number; 112 + totalActors: number; 113 113 message?: string; 114 114 } 115 115 ··· 199 199 protected async makeRequest<T = unknown>( 200 200 endpoint: string, 201 201 method?: "GET" | "POST" | "PUT" | "DELETE", 202 - params?: Record<string, unknown> | unknown 202 + params?: Record<string, unknown> | unknown, 203 203 ): Promise<T> { 204 204 return this.makeRequestWithRetry(endpoint, method, params, false); 205 205 } ··· 208 208 endpoint: string, 209 209 method?: "GET" | "POST" | "PUT" | "DELETE", 210 210 params?: Record<string, unknown> | unknown, 211 - isRetry?: boolean 211 + isRetry?: boolean, 212 212 ): Promise<T> { 213 213 isRetry = isRetry ?? false; 214 214 const httpMethod = method || "GET"; ··· 232 232 // For write operations, OAuth tokens are required 233 233 if (httpMethod !== "GET") { 234 234 throw new Error( 235 - `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 235 + `Authentication required: OAuth tokens are invalid or expired. Please log in again.`, 236 236 ); 237 237 } 238 238 ··· 267 267 268 268 // Handle 401 Unauthorized - attempt token refresh and retry once 269 269 if ( 270 - response.status === 401 && 271 - !isRetry && 272 - this.oauthClient && 270 + response.status === 401 && !isRetry && this.oauthClient && 273 271 httpMethod !== "GET" 274 272 ) { 275 273 try { 276 - // Mark current token as invalid to force refresh 277 - this.oauthClient.invalidateCurrentToken(); 278 274 // Force token refresh by calling ensureValidToken again 279 275 await this.oauthClient.ensureValidToken(); 280 276 // Retry the request once with refreshed tokens 281 277 return this.makeRequestWithRetry(endpoint, method, params, true); 282 278 } catch (_refreshError) { 283 279 throw new Error( 284 - `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 280 + `Authentication required: OAuth tokens are invalid or expired. Please log in again.`, 285 281 ); 286 282 } 287 283 } 288 284 289 285 throw new Error( 290 - `Request failed: ${response.status} ${response.statusText}` 286 + `Request failed: ${response.status} ${response.statusText}`, 291 287 ); 292 288 } 293 289 294 - return (await response.json()) as T; 290 + return await response.json() as T; 295 291 } 296 292 } 297 293 ··· 304 300 } 305 301 306 302 async listRecords( 307 - params?: ListRecordsParams 303 + params?: ListRecordsParams, 308 304 ): Promise<ListRecordsResponse<SocialSlicesSliceRecord>> { 309 305 const requestParams = { ...params, slice: this.sliceUri }; 310 306 return await this.makeRequest<ListRecordsResponse<SocialSlicesSliceRecord>>( 311 307 "social.slices.slice.list", 312 308 "GET", 313 - requestParams 309 + requestParams, 314 310 ); 315 311 } 316 312 317 313 async getRecord( 318 - params: GetRecordParams 314 + params: GetRecordParams, 319 315 ): Promise<RecordResponse<SocialSlicesSliceRecord>> { 320 316 const requestParams = { ...params, slice: this.sliceUri }; 321 317 return await this.makeRequest<RecordResponse<SocialSlicesSliceRecord>>( 322 318 "social.slices.slice.get", 323 319 "GET", 324 - requestParams 320 + requestParams, 325 321 ); 326 322 } 327 323 328 324 async searchRecords( 329 - params: SearchRecordsParams 325 + params: SearchRecordsParams, 330 326 ): Promise<ListRecordsResponse<SocialSlicesSliceRecord>> { 331 327 const requestParams = { ...params, slice: this.sliceUri }; 332 328 return await this.makeRequest<ListRecordsResponse<SocialSlicesSliceRecord>>( 333 329 "social.slices.slice.searchRecords", 334 330 "GET", 335 - requestParams 331 + requestParams, 336 332 ); 337 333 } 338 334 339 335 async createRecord( 340 336 record: SocialSlicesSliceRecord, 341 - useSelfRkey?: boolean 337 + useSelfRkey?: boolean, 342 338 ): Promise<{ uri: string; cid: string }> { 343 339 const recordWithType = { $type: "social.slices.slice", ...record }; 344 340 const payload = useSelfRkey ··· 347 343 return await this.makeRequest<{ uri: string; cid: string }>( 348 344 "social.slices.slice.create", 349 345 "POST", 350 - payload 346 + payload, 351 347 ); 352 348 } 353 349 354 350 async updateRecord( 355 351 rkey: string, 356 - record: SocialSlicesSliceRecord 352 + record: SocialSlicesSliceRecord, 357 353 ): Promise<{ uri: string; cid: string }> { 358 354 const recordWithType = { $type: "social.slices.slice", ...record }; 359 355 return await this.makeRequest<{ uri: string; cid: string }>( 360 356 "social.slices.slice.update", 361 357 "POST", 362 - { rkey, record: recordWithType } 358 + { rkey, record: recordWithType }, 363 359 ); 364 360 } 365 361 ··· 373 369 return await this.makeRequest<CodegenXrpcResponse>( 374 370 "social.slices.slice.codegen", 375 371 "POST", 376 - request 372 + request, 377 373 ); 378 374 } 379 375 ··· 381 377 return await this.makeRequest<BulkSyncOutput>( 382 378 "social.slices.slice.sync", 383 379 "POST", 384 - params 380 + params, 385 381 ); 386 382 } 387 383 ··· 389 385 return await this.makeRequest<SliceStatsOutput>( 390 386 "social.slices.slice.stats", 391 387 "POST", 392 - params 388 + params, 393 389 ); 394 390 } 395 391 ··· 397 393 return await this.makeRequest<SliceRecordsOutput>( 398 394 "social.slices.slice.records", 399 395 "POST", 400 - params 396 + params, 401 397 ); 402 398 } 403 399 } ··· 411 407 } 412 408 413 409 async listRecords( 414 - params?: ListRecordsParams 410 + params?: ListRecordsParams, 415 411 ): Promise<ListRecordsResponse<SocialSlicesLexiconRecord>> { 416 412 const requestParams = { ...params, slice: this.sliceUri }; 417 413 return await this.makeRequest< ··· 420 416 } 421 417 422 418 async getRecord( 423 - params: GetRecordParams 419 + params: GetRecordParams, 424 420 ): Promise<RecordResponse<SocialSlicesLexiconRecord>> { 425 421 const requestParams = { ...params, slice: this.sliceUri }; 426 422 return await this.makeRequest<RecordResponse<SocialSlicesLexiconRecord>>( 427 423 "social.slices.lexicon.get", 428 424 "GET", 429 - requestParams 425 + requestParams, 430 426 ); 431 427 } 432 428 433 429 async searchRecords( 434 - params: SearchRecordsParams 430 + params: SearchRecordsParams, 435 431 ): Promise<ListRecordsResponse<SocialSlicesLexiconRecord>> { 436 432 const requestParams = { ...params, slice: this.sliceUri }; 437 433 return await this.makeRequest< ··· 441 437 442 438 async createRecord( 443 439 record: SocialSlicesLexiconRecord, 444 - useSelfRkey?: boolean 440 + useSelfRkey?: boolean, 445 441 ): Promise<{ uri: string; cid: string }> { 446 442 const recordWithType = { $type: "social.slices.lexicon", ...record }; 447 443 const payload = useSelfRkey ··· 450 446 return await this.makeRequest<{ uri: string; cid: string }>( 451 447 "social.slices.lexicon.create", 452 448 "POST", 453 - payload 449 + payload, 454 450 ); 455 451 } 456 452 457 453 async updateRecord( 458 454 rkey: string, 459 - record: SocialSlicesLexiconRecord 455 + record: SocialSlicesLexiconRecord, 460 456 ): Promise<{ uri: string; cid: string }> { 461 457 const recordWithType = { $type: "social.slices.lexicon", ...record }; 462 458 return await this.makeRequest<{ uri: string; cid: string }>( 463 459 "social.slices.lexicon.update", 464 460 "POST", 465 - { rkey, record: recordWithType } 461 + { rkey, record: recordWithType }, 466 462 ); 467 463 } 468 464 ··· 470 466 return await this.makeRequest<void>( 471 467 "social.slices.lexicon.delete", 472 468 "POST", 473 - { rkey } 469 + { rkey }, 474 470 ); 475 471 } 476 472 } ··· 484 480 } 485 481 486 482 async listRecords( 487 - params?: ListRecordsParams 483 + params?: ListRecordsParams, 488 484 ): Promise<ListRecordsResponse<SocialSlicesActorProfileRecord>> { 489 485 const requestParams = { ...params, slice: this.sliceUri }; 490 486 return await this.makeRequest< ··· 493 489 } 494 490 495 491 async getRecord( 496 - params: GetRecordParams 492 + params: GetRecordParams, 497 493 ): Promise<RecordResponse<SocialSlicesActorProfileRecord>> { 498 494 const requestParams = { ...params, slice: this.sliceUri }; 499 495 return await this.makeRequest< ··· 502 498 } 503 499 504 500 async searchRecords( 505 - params: SearchRecordsParams 501 + params: SearchRecordsParams, 506 502 ): Promise<ListRecordsResponse<SocialSlicesActorProfileRecord>> { 507 503 const requestParams = { ...params, slice: this.sliceUri }; 508 504 return await this.makeRequest< ··· 512 508 513 509 async createRecord( 514 510 record: SocialSlicesActorProfileRecord, 515 - useSelfRkey?: boolean 511 + useSelfRkey?: boolean, 516 512 ): Promise<{ uri: string; cid: string }> { 517 513 const recordWithType = { $type: "social.slices.actor.profile", ...record }; 518 514 const payload = useSelfRkey ··· 521 517 return await this.makeRequest<{ uri: string; cid: string }>( 522 518 "social.slices.actor.profile.create", 523 519 "POST", 524 - payload 520 + payload, 525 521 ); 526 522 } 527 523 528 524 async updateRecord( 529 525 rkey: string, 530 - record: SocialSlicesActorProfileRecord 526 + record: SocialSlicesActorProfileRecord, 531 527 ): Promise<{ uri: string; cid: string }> { 532 528 const recordWithType = { $type: "social.slices.actor.profile", ...record }; 533 529 return await this.makeRequest<{ uri: string; cid: string }>( 534 530 "social.slices.actor.profile.update", 535 531 "POST", 536 - { rkey, record: recordWithType } 532 + { rkey, record: recordWithType }, 537 533 ); 538 534 } 539 535 ··· 541 537 return await this.makeRequest<void>( 542 538 "social.slices.actor.profile.delete", 543 539 "POST", 544 - { rkey } 540 + { rkey }, 545 541 ); 546 542 } 547 543 } ··· 556 552 this.profile = new ProfileActorSlicesSocialClient( 557 553 baseUrl, 558 554 sliceUri, 559 - oauthClient 555 + oauthClient, 560 556 ); 561 557 } 562 558 } ··· 574 570 this.lexicon = new LexiconSlicesSocialClient( 575 571 baseUrl, 576 572 sliceUri, 577 - oauthClient 573 + oauthClient, 578 574 ); 579 575 this.actor = new ActorSlicesSocialClient(baseUrl, sliceUri, oauthClient); 580 576 } ··· 609 605 610 606 private async uploadBlobWithRetry( 611 607 request: UploadBlobRequest, 612 - isRetry?: boolean 608 + isRetry?: boolean, 613 609 ): Promise<UploadBlobResponse> { 614 610 isRetry = isRetry ?? false; 615 611 // Special handling for blob upload with binary data ··· 636 632 // Handle 401 Unauthorized - attempt token refresh and retry once 637 633 if (response.status === 401 && !isRetry && this.oauthClient) { 638 634 try { 639 - // Mark current token as invalid to force refresh 640 - this.oauthClient.invalidateCurrentToken(); 641 635 // Force token refresh by calling ensureValidToken again 642 636 await this.oauthClient.ensureValidToken(); 643 637 // Retry the request once with refreshed tokens 644 638 return this.uploadBlobWithRetry(request, true); 645 639 } catch (_refreshError) { 646 640 throw new Error( 647 - `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 641 + `Authentication required: OAuth tokens are invalid or expired. Please log in again.`, 648 642 ); 649 643 } 650 644 } 651 645 652 646 throw new Error( 653 - `Blob upload failed: ${response.status} ${response.statusText}` 647 + `Blob upload failed: ${response.status} ${response.statusText}`, 654 648 ); 655 649 } 656 650
+2 -2
frontend/src/pages/SliceRecordsPage.tsx
··· 3 3 4 4 interface Record { 5 5 uri: string; 6 - indexed_at: string; 6 + indexedAt: string; 7 7 collection: string; 8 8 did: string; 9 9 cid: string; ··· 155 155 Indexed: 156 156 </dt> 157 157 <dd className="col-span-2 text-gray-900"> 158 - {new Date(record.indexed_at).toLocaleString()} 158 + {new Date(record.indexedAt).toLocaleString()} 159 159 </dd> 160 160 </div> 161 161 </dl>
+11 -11
frontend/src/routes/pages.tsx
··· 113 113 114 114 // Transform collection stats to match the interface 115 115 const collections = stats.success 116 - ? stats.collection_stats.map((stat) => ({ 116 + ? stats.collectionStats.map((stat) => ({ 117 117 name: stat.collection, 118 - count: stat.record_count, 119 - actors: stat.unique_actors, 118 + count: stat.recordCount, 119 + actors: stat.uniqueActors, 120 120 })) 121 121 : []; 122 122 123 123 sliceData = { 124 124 sliceId, 125 125 sliceName: sliceRecord.value.name, 126 - totalRecords: stats.success ? stats.total_records : 0, 127 - totalActors: stats.success ? stats.total_actors : 0, 128 - totalLexicons: stats.success ? stats.total_lexicons : 0, 126 + totalRecords: stats.success ? stats.totalRecords : 0, 127 + totalActors: stats.success ? stats.totalActors : 0, 128 + totalLexicons: stats.success ? stats.totalLexicons : 0, 129 129 collections, 130 130 }; 131 131 } catch (error) { ··· 189 189 190 190 // Transform collection stats to match the interface 191 191 const collections = stats.success 192 - ? stats.collection_stats.map((stat) => ({ 192 + ? stats.collectionStats.map((stat) => ({ 193 193 name: stat.collection, 194 - count: stat.record_count, 194 + count: stat.recordCount, 195 195 })) 196 196 : []; 197 197 198 198 sliceData = { 199 199 sliceId, 200 200 sliceName: sliceRecord.value.name, 201 - totalRecords: stats.success ? stats.total_records : 0, 201 + totalRecords: stats.success ? stats.totalRecords : 0, 202 202 collections, 203 203 }; 204 204 } catch (error) { ··· 219 219 // Fetch real records if a collection is selected 220 220 let records: Array<{ 221 221 uri: string; 222 - indexed_at: string; 222 + indexedAt: string; 223 223 collection: string; 224 224 did: string; 225 225 cid: string; ··· 246 246 if (recordsResult.success) { 247 247 records = recordsResult.records.map((record) => ({ 248 248 uri: record.uri, 249 - indexed_at: record.indexed_at, 249 + indexedAt: record.indexedAt, 250 250 collection: record.collection, 251 251 did: record.did, 252 252 cid: record.cid,
+5 -5
frontend/src/routes/slices.tsx
··· 375 375 nsid: lexicon.value.nsid, 376 376 definitions: lexicon.value.definitions, 377 377 uri: lexicon.uri, 378 - createdAt: lexicon.indexed_at 378 + createdAt: lexicon.indexedAt 379 379 }); 380 380 const html = render(component); 381 381 ··· 482 482 483 483 const component = await CodegenResult({ 484 484 success: result.success, 485 - generatedCode: result.generated_code, 485 + generatedCode: result.generatedCode, 486 486 error: result.error 487 487 }); 488 488 const html = render(component); ··· 679 679 <SyncResult 680 680 success={syncResult.success} 681 681 message={syncResult.message} 682 - collectionsCount={syncResult.collections_synced?.length || 0} 683 - reposProcessed={syncResult.repos_processed || 0} 684 - totalRecords={syncResult.total_records || 0} 682 + collectionsCount={syncResult.collectionsSynced?.length || 0} 683 + reposProcessed={syncResult.reposProcessed || 0} 684 + totalRecords={syncResult.totalRecords || 0} 685 685 error={syncResult.success ? undefined : syncResult.message} 686 686 /> 687 687 );