Spaces:
Sleeping
Sleeping
Sasha commited on
Commit ·
91f86ea
1
Parent(s): 80fc693
Optimize KPI cards to request overall stats summary from /api/stats/summary and display true totals.
Browse files- client/src/App.jsx +13 -14
- server/db.js +84 -0
- server/server.js +13 -0
client/src/App.jsx
CHANGED
|
@@ -300,6 +300,7 @@ export default function App() {
|
|
| 300 |
const [activityData, setActivityData] = useState([]);
|
| 301 |
const [modActions, setModActions] = useState([]);
|
| 302 |
const [modSummary, setModSummary] = useState([]);
|
|
|
|
| 303 |
|
| 304 |
// RPG Mod Profiles
|
| 305 |
const [modProfiles, setModProfiles] = useState([]);
|
|
@@ -520,6 +521,17 @@ export default function App() {
|
|
| 520 |
const chattersData = await chattersRes.json();
|
| 521 |
setChatters(Array.isArray(chattersData) ? chattersData : []);
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
// 2. Spoken voice words
|
| 524 |
const voiceRes = await fetch(getUrl('/api/stats/words', { type: 'voice', limit: 60 }));
|
| 525 |
const voiceData = await voiceRes.json();
|
|
@@ -613,20 +625,7 @@ export default function App() {
|
|
| 613 |
});
|
| 614 |
};
|
| 615 |
|
| 616 |
-
|
| 617 |
-
const totalMessages = chatters.reduce((acc, curr) => acc + curr.message_count, 0);
|
| 618 |
-
const totalUniqueChatters = chatters.length;
|
| 619 |
-
const topChatter = chatters[0]?.display_name || '—';
|
| 620 |
-
const totalVoiceWords = voiceWords.reduce((acc, curr) => acc + curr.word_count, 0);
|
| 621 |
-
const totalModActionsCount = modActions.length;
|
| 622 |
-
|
| 623 |
-
const statsSummary = {
|
| 624 |
-
messages: totalMessages,
|
| 625 |
-
uniqueChatters: totalUniqueChatters,
|
| 626 |
-
mostActiveChatter: topChatter,
|
| 627 |
-
voiceWordsCount: totalVoiceWords,
|
| 628 |
-
modActionsCount: totalModActionsCount
|
| 629 |
-
};
|
| 630 |
|
| 631 |
// Chart Setup
|
| 632 |
const chartLabels = activityData.map(d => {
|
|
|
|
| 300 |
const [activityData, setActivityData] = useState([]);
|
| 301 |
const [modActions, setModActions] = useState([]);
|
| 302 |
const [modSummary, setModSummary] = useState([]);
|
| 303 |
+
const [statsSummary, setStatsSummary] = useState({ messages: 0, uniqueChatters: 0, mostActiveChatter: '—', voiceWordsCount: 0, modActionsCount: 0 });
|
| 304 |
|
| 305 |
// RPG Mod Profiles
|
| 306 |
const [modProfiles, setModProfiles] = useState([]);
|
|
|
|
| 521 |
const chattersData = await chattersRes.json();
|
| 522 |
setChatters(Array.isArray(chattersData) ? chattersData : []);
|
| 523 |
|
| 524 |
+
// 1.5. Overall stream stats summary (true totals)
|
| 525 |
+
try {
|
| 526 |
+
const statsSummaryRes = await fetch(getUrl('/api/stats/summary'));
|
| 527 |
+
const statsSummaryData = await statsSummaryRes.json();
|
| 528 |
+
if (statsSummaryData && !statsSummaryData.error) {
|
| 529 |
+
setStatsSummary(statsSummaryData);
|
| 530 |
+
}
|
| 531 |
+
} catch (err) {
|
| 532 |
+
console.error('Error fetching stream stats summary:', err);
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
// 2. Spoken voice words
|
| 536 |
const voiceRes = await fetch(getUrl('/api/stats/words', { type: 'voice', limit: 60 }));
|
| 537 |
const voiceData = await voiceRes.json();
|
|
|
|
| 625 |
});
|
| 626 |
};
|
| 627 |
|
| 628 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
// Chart Setup
|
| 631 |
const chartLabels = activityData.map(d => {
|
server/db.js
CHANGED
|
@@ -991,6 +991,90 @@ export async function getTopChatters(streamId = null, limit = 50) {
|
|
| 991 |
}
|
| 992 |
}
|
| 993 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
/**
|
| 995 |
* Get top spoken words
|
| 996 |
*/
|
|
|
|
| 991 |
}
|
| 992 |
}
|
| 993 |
|
| 994 |
+
/**
|
| 995 |
+
* Get overall summary statistics (true total metrics)
|
| 996 |
+
*/
|
| 997 |
+
export async function getStatsSummary(streamId = null) {
|
| 998 |
+
if (dbMode === 'sqlite') {
|
| 999 |
+
let msgCount, uniqueChatters, voiceCount, topChatter;
|
| 1000 |
+
|
| 1001 |
+
if (streamId) {
|
| 1002 |
+
msgCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM messages WHERE stream_id = ?').get(streamId).count;
|
| 1003 |
+
uniqueChatters = sqliteDb.prepare('SELECT COUNT(DISTINCT username) as count FROM messages WHERE stream_id = ?').get(streamId).count;
|
| 1004 |
+
voiceCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM voice_words WHERE stream_id = ?').get(streamId).count;
|
| 1005 |
+
const row = sqliteDb.prepare('SELECT display_name FROM stream_viewers WHERE stream_id = ? AND has_chatted = 1 ORDER BY (SELECT COUNT(*) FROM messages m WHERE m.stream_id = ? AND m.username = stream_viewers.username) DESC LIMIT 1').get(streamId, streamId);
|
| 1006 |
+
topChatter = row ? row.display_name : '—';
|
| 1007 |
+
} else {
|
| 1008 |
+
msgCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM messages').get().count;
|
| 1009 |
+
uniqueChatters = sqliteDb.prepare('SELECT COUNT(DISTINCT username) as count FROM messages').get().count;
|
| 1010 |
+
voiceCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM voice_words').get().count;
|
| 1011 |
+
const row = sqliteDb.prepare('SELECT username FROM messages GROUP BY username ORDER BY COUNT(*) DESC LIMIT 1').get();
|
| 1012 |
+
topChatter = row ? row.username : '—';
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
return {
|
| 1016 |
+
messages: msgCount,
|
| 1017 |
+
uniqueChatters: uniqueChatters,
|
| 1018 |
+
mostActiveChatter: topChatter,
|
| 1019 |
+
voiceWordsCount: voiceCount
|
| 1020 |
+
};
|
| 1021 |
+
} else {
|
| 1022 |
+
// Supabase Mode
|
| 1023 |
+
let msgCountQuery = supabase.from('messages').select('*', { count: 'exact', head: true });
|
| 1024 |
+
let voiceCountQuery = supabase.from('voice_words').select('*', { count: 'exact', head: true });
|
| 1025 |
+
|
| 1026 |
+
if (streamId) {
|
| 1027 |
+
msgCountQuery = msgCountQuery.eq('stream_id', streamId);
|
| 1028 |
+
voiceCountQuery = voiceCountQuery.eq('stream_id', streamId);
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
const { count: msgCount } = await msgCountQuery;
|
| 1032 |
+
const { count: voiceCount } = await voiceCountQuery;
|
| 1033 |
+
|
| 1034 |
+
// Unique chatters count
|
| 1035 |
+
let uniqueChatters = 0;
|
| 1036 |
+
if (streamId) {
|
| 1037 |
+
const { count } = await supabase
|
| 1038 |
+
.from('stream_chatter_stats')
|
| 1039 |
+
.select('*', { count: 'exact', head: true })
|
| 1040 |
+
.eq('stream_id', streamId);
|
| 1041 |
+
uniqueChatters = count || 0;
|
| 1042 |
+
} else {
|
| 1043 |
+
const { count } = await supabase
|
| 1044 |
+
.from('global_chatter_stats')
|
| 1045 |
+
.select('*', { count: 'exact', head: true });
|
| 1046 |
+
uniqueChatters = count || 0;
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
// Top active chatter
|
| 1050 |
+
let topChatter = '—';
|
| 1051 |
+
if (streamId) {
|
| 1052 |
+
const { data } = await supabase
|
| 1053 |
+
.from('stream_chatter_stats')
|
| 1054 |
+
.select('display_name')
|
| 1055 |
+
.eq('stream_id', streamId)
|
| 1056 |
+
.order('message_count', { ascending: false })
|
| 1057 |
+
.limit(1);
|
| 1058 |
+
if (data && data.length > 0) topChatter = data[0].display_name;
|
| 1059 |
+
} else {
|
| 1060 |
+
const { data } = await supabase
|
| 1061 |
+
.from('global_chatter_stats')
|
| 1062 |
+
.select('username')
|
| 1063 |
+
.order('message_count', { ascending: false })
|
| 1064 |
+
.limit(1);
|
| 1065 |
+
if (data && data.length > 0) topChatter = data[0].username;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
return {
|
| 1069 |
+
messages: msgCount || 0,
|
| 1070 |
+
uniqueChatters: uniqueChatters,
|
| 1071 |
+
mostActiveChatter: topChatter,
|
| 1072 |
+
voiceWordsCount: voiceCount || 0
|
| 1073 |
+
};
|
| 1074 |
+
}
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
/**
|
| 1079 |
* Get top spoken words
|
| 1080 |
*/
|
server/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
| 23 |
getModActionsSummaryRaw,
|
| 24 |
syncActiveStream,
|
| 25 |
getOrStartActiveStream,
|
|
|
|
| 26 |
getModeratorProfilesData,
|
| 27 |
getActiveStream,
|
| 28 |
getPendingBackfillStreams,
|
|
@@ -519,6 +520,18 @@ app.get('/api/stats/chatters', cacheMiddleware(), async (req, res) => {
|
|
| 519 |
res.json(chatters);
|
| 520 |
});
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
// Stop words arrays (Russian and English) to filter out common articles, prepositions
|
| 523 |
const STOP_WORDS = new Set([
|
| 524 |
// Russian
|
|
|
|
| 23 |
getModActionsSummaryRaw,
|
| 24 |
syncActiveStream,
|
| 25 |
getOrStartActiveStream,
|
| 26 |
+
getStatsSummary,
|
| 27 |
getModeratorProfilesData,
|
| 28 |
getActiveStream,
|
| 29 |
getPendingBackfillStreams,
|
|
|
|
| 520 |
res.json(chatters);
|
| 521 |
});
|
| 522 |
|
| 523 |
+
// Get true total summary statistics
|
| 524 |
+
app.get('/api/stats/summary', cacheMiddleware(), async (req, res) => {
|
| 525 |
+
const streamId = req.query.stream_id ? parseInt(req.query.stream_id) : null;
|
| 526 |
+
try {
|
| 527 |
+
const summary = await getStatsSummary(streamId);
|
| 528 |
+
res.json(summary);
|
| 529 |
+
} catch (err) {
|
| 530 |
+
console.error('[API Error] /api/stats/summary:', err);
|
| 531 |
+
res.status(500).json({ error: 'Internal Server Error' });
|
| 532 |
+
}
|
| 533 |
+
});
|
| 534 |
+
|
| 535 |
// Stop words arrays (Russian and English) to filter out common articles, prepositions
|
| 536 |
const STOP_WORDS = new Set([
|
| 537 |
// Russian
|