A third party ATProto appview

display fixes

+92 -25
+3 -1
.claude/settings.local.json
··· 18 18 "Bash(yarn install)", 19 19 "Bash(node --version:*)", 20 20 "Bash(npm:*)", 21 - "Bash(docker-compose restart:*)" 21 + "Bash(docker-compose restart:*)", 22 + "Bash(echo \"# Bluesky Branding Removal Checklist\n\n## 1. App Icons & Images\n- assets/app-icons/*.png - App icons (replace with Aurora Prism branding)\n- assets/favicon.png - Browser favicon\n- assets/icon-android-*.png - Android icons\n- assets/default-avatar.png - Default avatar image\n\n## 2. App Metadata\n- app.json - App name, slug, description\n- package.json - App name and description\n\n## 3. Text References (276 occurrences)\n- Onboarding screens (src/screens/Onboarding/)\n- Signup screens (src/screens/Signup/)\n- Settings/About screens\n- Terms of Service / Privacy Policy references\n- Help text and tooltips\n- Error messages mentioning Bluesky\n\n## 4. URLs\n- bsky.app references (feed URLs, profile URLs)\n- bsky.social references\n- Links to Bluesky support/help\n\n## 5. Service Names\n- Bluesky Moderation Service references\n- Default feed generator names\n\nTotal: 276 text references found\")", 23 + "Bash(psql \"$DATABASE_URL\" -c \"SELECT \n (SELECT COUNT(*) FROM users) as users,\n (SELECT COUNT(*) FROM posts) as posts,\n (SELECT COUNT(*) FROM likes) as likes,\n (SELECT COUNT(*) FROM reposts) as reposts,\n (SELECT COUNT(*) FROM follows) as follows,\n (SELECT COUNT(*) FROM blocks) as blocks;\")" 22 24 ], 23 25 "deny": [], 24 26 "ask": []
+20
server/routes.ts
··· 4323 4323 }); 4324 4324 }); 4325 4325 4326 + // Force stats refresh endpoint - clears cache and recalculates from database 4327 + app.post('/api/metrics/refresh', async (_req, res) => { 4328 + try { 4329 + // Clear the stats cache to force a fresh calculation 4330 + storage.clearStatsCache(); 4331 + 4332 + // Get fresh stats (will query database) 4333 + const stats = await storage.getStats(); 4334 + 4335 + res.json({ 4336 + success: true, 4337 + message: 'Stats refreshed successfully', 4338 + stats 4339 + }); 4340 + } catch (err) { 4341 + console.error('[METRICS] Error refreshing stats:', err); 4342 + res.status(500).json({ error: 'InternalServerError', message: 'Failed to refresh stats' }); 4343 + } 4344 + }); 4345 + 4326 4346 // Minimal dead-letter inspection endpoint (admin only in production) 4327 4347 app.get('/api/redis/dead-letters', async (_req, res) => { 4328 4348 try {
+12
server/services/redis-queue.ts
··· 760 760 } 761 761 } 762 762 763 + async setRecordCount(table: string, value: number) { 764 + if (!this.redis || !this.isInitialized) { 765 + return; 766 + } 767 + 768 + try { 769 + await this.redis.hset('db:record_counts', table, value.toString()); 770 + } catch (error) { 771 + console.error('[REDIS] Error setting record count:', error); 772 + } 773 + } 774 + 763 775 async getRecordCounts(): Promise<Record<string, number>> { 764 776 if (!this.redis || !this.isInitialized) { 765 777 return {};
+57 -24
server/storage.ts
··· 3864 3864 }); 3865 3865 } 3866 3866 3867 + clearStatsCache() { 3868 + console.log('[STORAGE] Clearing stats cache'); 3869 + this.statsCache = null; 3870 + this.statsQueryInProgress = false; 3871 + } 3872 + 3867 3873 private async refreshStatsInBackground() { 3868 3874 // Skip if query is already in progress 3869 3875 if (this.statsQueryInProgress) { ··· 3907 3913 ) { 3908 3914 this.refreshStatsInBackground(); 3909 3915 } 3916 + console.log('[STORAGE] Returning cached stats:', this.statsCache.data); 3910 3917 return this.statsCache.data; 3911 3918 } 3912 3919 3913 3920 // No cache yet - try Redis counters first (fast) 3914 3921 const { redisQueue } = await import('./services/redis-queue'); 3915 3922 const redisCounts = await redisQueue.getRecordCounts(); 3923 + 3924 + console.log('[STORAGE] Redis counts:', redisCounts); 3916 3925 3917 3926 if (Object.keys(redisCounts).length > 0) { 3918 3927 const data = { ··· 3925 3934 }; 3926 3935 // Cache it 3927 3936 this.statsCache = { data, timestamp: Date.now() }; 3937 + console.log('[STORAGE] Using Redis counts:', data); 3928 3938 return data; 3929 3939 } 3930 3940 3941 + console.log('[STORAGE] No Redis counts, falling back to PostgreSQL COUNT queries'); 3942 + 3931 3943 // No Redis counts - fallback to PostgreSQL query (only on first load) 3932 3944 if (this.statsQueryInProgress) { 3933 3945 // Another request is already fetching, return zeros ··· 3944 3956 this.statsQueryInProgress = true; 3945 3957 3946 3958 try { 3947 - // Add 5 second timeout to prevent blocking 3959 + // Use accurate COUNT(*) queries instead of pg_stat_user_tables estimates 3960 + // Run all counts in a single query for better performance 3948 3961 const statsPromise = this.db.execute<{ 3949 - schemaname: string; 3950 - relname: string; 3951 - count: number; 3962 + users: string; 3963 + posts: string; 3964 + likes: string; 3965 + reposts: string; 3966 + follows: string; 3967 + blocks: string; 3952 3968 }>(sql` 3953 - SELECT 3954 - schemaname, 3955 - relname, 3956 - n_live_tup as count 3957 - FROM pg_stat_user_tables 3958 - WHERE schemaname = 'public' 3959 - AND relname IN ('users', 'posts', 'likes', 'reposts', 'follows', 'blocks') 3969 + SELECT 3970 + (SELECT COUNT(*)::text FROM users) as users, 3971 + (SELECT COUNT(*)::text FROM posts) as posts, 3972 + (SELECT COUNT(*)::text FROM likes) as likes, 3973 + (SELECT COUNT(*)::text FROM reposts) as reposts, 3974 + (SELECT COUNT(*)::text FROM follows) as follows, 3975 + (SELECT COUNT(*)::text FROM blocks) as blocks 3960 3976 `); 3961 3977 3962 3978 const timeoutPromise = new Promise<never>((_, reject) => { 3963 - setTimeout(() => reject(new Error('Stats query timeout')), 5000); 3979 + setTimeout(() => reject(new Error('Stats query timeout')), 10000); 3964 3980 }); 3965 3981 3966 3982 const result = await Promise.race([statsPromise, timeoutPromise]); 3967 3983 3968 - const stats: Record<string, number> = {}; 3969 - result.rows.forEach( 3970 - (row: { relname: string; count: string | number }) => { 3971 - stats[row.relname] = Number(row.count || 0); 3972 - } 3973 - ); 3984 + const row = result.rows[0] as { 3985 + users?: string; 3986 + posts?: string; 3987 + likes?: string; 3988 + reposts?: string; 3989 + follows?: string; 3990 + blocks?: string; 3991 + }; 3974 3992 3975 3993 const data = { 3976 - totalUsers: stats.users || 0, 3977 - totalPosts: stats.posts || 0, 3978 - totalLikes: stats.likes || 0, 3979 - totalReposts: stats.reposts || 0, 3980 - totalFollows: stats.follows || 0, 3981 - totalBlocks: stats.blocks || 0, 3994 + totalUsers: parseInt(row.users || '0'), 3995 + totalPosts: parseInt(row.posts || '0'), 3996 + totalLikes: parseInt(row.likes || '0'), 3997 + totalReposts: parseInt(row.reposts || '0'), 3998 + totalFollows: parseInt(row.follows || '0'), 3999 + totalBlocks: parseInt(row.blocks || '0'), 3982 4000 }; 3983 4001 4002 + console.log('[STORAGE] PostgreSQL COUNT query result:', data); 4003 + 3984 4004 // Cache the result 3985 4005 this.statsCache = { data, timestamp: Date.now() }; 3986 4006 this.statsQueryInProgress = false; 4007 + 4008 + // Update Redis counters with accurate counts 4009 + const { redisQueue: redisQueueUpdate } = await import('./services/redis-queue'); 4010 + await Promise.all([ 4011 + redisQueueUpdate.setRecordCount('users', data.totalUsers), 4012 + redisQueueUpdate.setRecordCount('posts', data.totalPosts), 4013 + redisQueueUpdate.setRecordCount('likes', data.totalLikes), 4014 + redisQueueUpdate.setRecordCount('reposts', data.totalReposts), 4015 + redisQueueUpdate.setRecordCount('follows', data.totalFollows), 4016 + redisQueueUpdate.setRecordCount('blocks', data.totalBlocks), 4017 + ]).catch((err) => { 4018 + console.error('[STORAGE] Failed to update Redis counters:', err); 4019 + }); 3987 4020 3988 4021 return data; 3989 4022 } catch (error) {