Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

clean up admin db view

+182 -26
+130 -22
apps/main-app/public/admin/admin.tsx
··· 127 127 const [database, setDatabase] = useState<any>(null) 128 128 const [sites, setSites] = useState<any>(null) 129 129 const [health, setHealth] = useState<any>(null) 130 + const [firehose, setFirehose] = useState<any>(null) 130 131 const [autoRefresh, setAutoRefresh] = useState(true) 131 132 132 133 // Filters ··· 190 191 } 191 192 } 192 193 194 + const fetchFirehose = async () => { 195 + const res = await fetch('/api/admin/firehose', { credentials: 'include' }) 196 + if (res.ok) { 197 + const data = await res.json() 198 + setFirehose(data) 199 + } 200 + } 201 + 193 202 const logout = async () => { 194 203 await fetch('/api/admin/logout', { method: 'POST', credentials: 'include' }) 195 204 window.location.reload() ··· 199 208 fetchMetrics() 200 209 fetchDatabase() 201 210 fetchHealth() 211 + fetchFirehose() 202 212 fetchLogs() 203 213 fetchErrors() 204 214 fetchSites() ··· 215 225 if (tab === 'overview') { 216 226 fetchMetrics() 217 227 fetchHealth() 228 + fetchFirehose() 218 229 } else if (tab === 'logs') { 219 230 fetchLogs() 220 231 } else if (tab === 'errors') { ··· 307 318 </div> 308 319 )} 309 320 321 + {/* Firehose Worker */} 322 + {firehose && ( 323 + <div> 324 + <h2 className="text-xl font-bold mb-4">Firehose Worker</h2> 325 + <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 326 + <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 327 + <div> 328 + <div className="text-sm text-gray-400">Status</div> 329 + <div className="flex items-center gap-2 mt-1"> 330 + <span className={`inline-block w-3 h-3 rounded-full ${firehose.firehose?.healthy ? 'bg-green-500' : 'bg-red-500'}`}></span> 331 + <span className="text-lg font-bold">{firehose.firehose?.connected ? 'Connected' : 'Disconnected'}</span> 332 + </div> 333 + </div> 334 + <div> 335 + <div className="text-sm text-gray-400">Mode</div> 336 + <div className="text-lg font-bold capitalize">{firehose.mode || 'unknown'}</div> 337 + </div> 338 + <div> 339 + <div className="text-sm text-gray-400">Queue Size</div> 340 + <div className="text-lg font-bold">{firehose.firehose?.queueSize || 0}</div> 341 + </div> 342 + <div> 343 + <div className="text-sm text-gray-400">Active Handlers</div> 344 + <div className="text-lg font-bold">{firehose.firehose?.activeHandlers || 0}</div> 345 + </div> 346 + </div> 347 + {firehose.firehose?.lastEventTime && ( 348 + <div className="mt-3 text-sm text-gray-400"> 349 + Last event: {new Date(firehose.firehose.lastEventTime).toLocaleString()} 350 + ({Math.round(firehose.firehose.timeSinceLastEvent / 1000)}s ago) 351 + </div> 352 + )} 353 + </div> 354 + </div> 355 + )} 356 + 310 357 {/* Metrics */} 311 358 {metrics && ( 312 359 <div> ··· 527 574 {tab === 'database' && database && ( 528 575 <div className="space-y-6"> 529 576 {/* Stats */} 530 - <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> 577 + <div className="grid grid-cols-2 md:grid-cols-5 gap-4"> 531 578 <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 532 579 <div className="text-sm text-gray-400 mb-1">Total Sites</div> 533 580 <div className="text-3xl font-bold">{database.stats.totalSites}</div> ··· 540 587 <div className="text-sm text-gray-400 mb-1">Custom Domains</div> 541 588 <div className="text-3xl font-bold">{database.stats.totalCustomDomains}</div> 542 589 </div> 590 + <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 591 + <div className="text-sm text-gray-400 mb-1">Site Cache</div> 592 + <div className="text-3xl font-bold">{database.stats.totalSiteCache}</div> 593 + </div> 594 + <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 595 + <div className="text-sm text-gray-400 mb-1">Settings Cache</div> 596 + <div className="text-3xl font-bold">{database.stats.totalSiteSettingsCache}</div> 597 + </div> 543 598 </div> 544 599 545 600 {/* Recent Sites */} ··· 550 605 <thead className="bg-gray-800"> 551 606 <tr> 552 607 <th className="px-4 py-2 text-left">Site Name</th> 553 - <th className="px-4 py-2 text-left">Subdomain</th> 608 + <th className="px-4 py-2 text-left">Links</th> 554 609 <th className="px-4 py-2 text-left">DID</th> 555 610 <th className="px-4 py-2 text-left">RKey</th> 556 611 <th className="px-4 py-2 text-left">Created</th> 612 + <th className="px-4 py-2 text-left">PDSls</th> 557 613 </tr> 558 614 </thead> 559 615 <tbody> ··· 561 617 <tr key={i} className="border-t border-gray-800"> 562 618 <td className="px-4 py-2">{site.display_name || 'Untitled'}</td> 563 619 <td className="px-4 py-2"> 564 - {site.subdomain ? ( 620 + <div className="flex flex-col gap-1"> 565 621 <a 566 - href={`https://${site.subdomain}`} 622 + href={`https://sites.wisp.place/${site.did}/${site.rkey || 'self'}`} 567 623 target="_blank" 568 624 rel="noopener noreferrer" 569 - className="text-blue-400 hover:underline" 625 + className="text-blue-400 hover:underline text-xs" 570 626 > 571 - {site.subdomain} 627 + sites.wisp.place 572 628 </a> 573 - ) : ( 574 - <span className="text-gray-500">No domain</span> 575 - )} 629 + {site.subdomain && ( 630 + <a 631 + href={`https://${site.subdomain}`} 632 + target="_blank" 633 + rel="noopener noreferrer" 634 + className="text-green-400 hover:underline text-xs" 635 + > 636 + {site.subdomain} 637 + </a> 638 + )} 639 + {site.custom_domain && ( 640 + <a 641 + href={`https://${site.custom_domain}`} 642 + target="_blank" 643 + rel="noopener noreferrer" 644 + className="text-purple-400 hover:underline text-xs" 645 + > 646 + {site.custom_domain} 647 + </a> 648 + )} 649 + </div> 576 650 </td> 577 651 <td className="px-4 py-2 text-gray-400 font-mono text-xs"> 578 - {site.did.slice(0, 20)}... 652 + {site.did} 579 653 </td> 580 654 <td className="px-4 py-2 text-gray-400">{site.rkey || 'self'}</td> 581 655 <td className="px-4 py-2 text-gray-400"> ··· 610 684 <tr> 611 685 <th className="px-4 py-2 text-left">Domain</th> 612 686 <th className="px-4 py-2 text-left">DID</th> 687 + <th className="px-4 py-2 text-left">RKey</th> 613 688 <th className="px-4 py-2 text-left">Verified</th> 614 689 <th className="px-4 py-2 text-left">Created</th> 615 690 </tr> ··· 617 692 <tbody> 618 693 {database.recentDomains.map((domain: any, i: number) => ( 619 694 <tr key={i} className="border-t border-gray-800"> 620 - <td className="px-4 py-2">{domain.domain}</td> 695 + <td className="px-4 py-2"> 696 + {domain.verified ? ( 697 + <a 698 + href={`https://${domain.domain}`} 699 + target="_blank" 700 + rel="noopener noreferrer" 701 + className="text-blue-400 hover:underline" 702 + > 703 + {domain.domain} 704 + </a> 705 + ) : ( 706 + <span className="text-gray-400">{domain.domain}</span> 707 + )} 708 + </td> 621 709 <td className="px-4 py-2 text-gray-400 font-mono text-xs"> 622 - {domain.did.slice(0, 20)}... 710 + {domain.did} 623 711 </td> 712 + <td className="px-4 py-2 text-gray-400">{domain.rkey || 'self'}</td> 624 713 <td className="px-4 py-2"> 625 714 <span 626 715 className={`px-2 py-1 rounded text-xs ${ ··· 654 743 <thead className="bg-gray-800"> 655 744 <tr> 656 745 <th className="px-4 py-2 text-left">Site Name</th> 657 - <th className="px-4 py-2 text-left">Subdomain</th> 746 + <th className="px-4 py-2 text-left">Links</th> 658 747 <th className="px-4 py-2 text-left">DID</th> 659 748 <th className="px-4 py-2 text-left">RKey</th> 660 749 <th className="px-4 py-2 text-left">Created</th> 750 + <th className="px-4 py-2 text-left">PDSls</th> 661 751 </tr> 662 752 </thead> 663 753 <tbody> ··· 665 755 <tr key={i} className="border-t border-gray-800 hover:bg-gray-800"> 666 756 <td className="px-4 py-2">{site.display_name || 'Untitled'}</td> 667 757 <td className="px-4 py-2"> 668 - {site.subdomain ? ( 758 + <div className="flex flex-col gap-1"> 669 759 <a 670 - href={`https://${site.subdomain}`} 760 + href={`https://sites.wisp.place/${site.did}/${site.rkey || 'self'}`} 671 761 target="_blank" 672 762 rel="noopener noreferrer" 673 - className="text-blue-400 hover:underline" 763 + className="text-blue-400 hover:underline text-xs" 674 764 > 675 - {site.subdomain} 765 + sites.wisp.place 676 766 </a> 677 - ) : ( 678 - <span className="text-gray-500">No domain</span> 679 - )} 767 + {site.subdomain && ( 768 + <a 769 + href={`https://${site.subdomain}`} 770 + target="_blank" 771 + rel="noopener noreferrer" 772 + className="text-green-400 hover:underline text-xs" 773 + > 774 + {site.subdomain} 775 + </a> 776 + )} 777 + {site.custom_domain && ( 778 + <a 779 + href={`https://${site.custom_domain}`} 780 + target="_blank" 781 + rel="noopener noreferrer" 782 + className="text-purple-400 hover:underline text-xs" 783 + > 784 + {site.custom_domain} 785 + </a> 786 + )} 787 + </div> 680 788 </td> 681 789 <td className="px-4 py-2 text-gray-400 font-mono text-xs"> 682 - {site.did.slice(0, 30)}... 790 + {site.did} 683 791 </td> 684 792 <td className="px-4 py-2 text-gray-400">{site.rkey || 'self'}</td> 685 793 <td className="px-4 py-2 text-gray-400"> ··· 749 857 </span> 750 858 </td> 751 859 <td className="px-4 py-2 text-gray-400 font-mono text-xs"> 752 - {domain.did.slice(0, 30)}... 860 + {domain.did} 753 861 </td> 754 862 <td className="px-4 py-2 text-gray-400">{domain.rkey || 'self'}</td> 755 863 <td className="px-4 py-2 text-gray-400">
+52 -4
apps/main-app/src/routes/admin.ts
··· 270 270 // Get database stats (protected) 271 271 /** 272 272 * GET /api/admin/database 273 - * Success: { stats, recentSites, recentDomains } 273 + * Success: { stats, recentSites, recentDomains, siteCacheStats } 274 274 * Failure (500): { error, message } 275 275 */ 276 276 .get('/database', async ({ cookie, set }) => { ··· 282 282 const allSitesResult = await db`SELECT COUNT(*) as count FROM sites` 283 283 const wispSubdomainsResult = await db`SELECT COUNT(*) as count FROM domains WHERE domain LIKE '%.wisp.place'` 284 284 const customDomainsResult = await db`SELECT COUNT(*) as count FROM custom_domains WHERE verified = true` 285 + const siteCacheResult = await db`SELECT COUNT(*) as count FROM site_cache` 286 + const siteSettingsCacheResult = await db`SELECT COUNT(*) as count FROM site_settings_cache` 285 287 286 288 // Get recent sites (including those without domains) 287 289 const recentSites = await db` ··· 290 292 s.rkey, 291 293 s.display_name, 292 294 s.created_at, 293 - d.domain as subdomain 295 + d.domain as subdomain, 296 + cd.domain as custom_domain 294 297 FROM sites s 295 298 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place' 299 + LEFT JOIN custom_domains cd ON s.did = cd.did AND s.rkey = cd.rkey AND cd.verified = true 296 300 ORDER BY s.created_at DESC 297 301 LIMIT 10 298 302 ` ··· 304 308 stats: { 305 309 totalSites: allSitesResult[0].count, 306 310 totalWispSubdomains: wispSubdomainsResult[0].count, 307 - totalCustomDomains: customDomainsResult[0].count 311 + totalCustomDomains: customDomainsResult[0].count, 312 + totalSiteCache: siteCacheResult[0].count, 313 + totalSiteSettingsCache: siteSettingsCacheResult[0].count 308 314 }, 309 315 recentSites: recentSites, 310 316 recentDomains: recentDomains ··· 385 391 s.rkey, 386 392 s.display_name, 387 393 s.created_at, 388 - d.domain as subdomain 394 + d.domain as subdomain, 395 + cd.domain as custom_domain 389 396 FROM sites s 390 397 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place' 398 + LEFT JOIN custom_domains cd ON s.did = cd.did AND s.rkey = cd.rkey AND cd.verified = true 391 399 ORDER BY s.created_at DESC 392 400 LIMIT ${limit} OFFSET ${offset} 393 401 ` ··· 412 420 set.status = 500 413 421 return { 414 422 error: 'Failed to fetch sites', 423 + message: error instanceof Error ? error.message : String(error) 424 + } 425 + } 426 + }, { 427 + cookie: t.Cookie({ 428 + admin_session: t.Optional(t.String()) 429 + }, { 430 + secrets: cookieSecret, 431 + sign: ['admin_session'] 432 + }) 433 + }) 434 + 435 + // Get firehose worker status (protected) 436 + /** 437 + * GET /api/admin/firehose 438 + * Success: firehose health data from firehose-service 439 + * Failure (503|500): { error, message } 440 + */ 441 + .get('/firehose', async ({ cookie, set }) => { 442 + const check = requireAdmin({ cookie, set }) 443 + if (check) return check 444 + 445 + try { 446 + const firehoseServiceUrl = process.env.FIREHOSE_SERVICE_URL || `http://localhost:${process.env.FIREHOSE_PORT || '3002'}` 447 + const response = await fetch(`${firehoseServiceUrl}/health`) 448 + 449 + if (response.ok) { 450 + const data = await response.json() 451 + return data 452 + } else { 453 + set.status = 503 454 + return { 455 + error: 'Failed to fetch firehose status', 456 + message: 'Firehose service unavailable' 457 + } 458 + } 459 + } catch (error) { 460 + set.status = 500 461 + return { 462 + error: 'Failed to fetch firehose status', 415 463 message: error instanceof Error ? error.message : String(error) 416 464 } 417 465 }