GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of GameSentenceMiner might be problematic. Click here for more details.

Files changed (76) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +6 -6
  2. GameSentenceMiner/anki.py +236 -152
  3. GameSentenceMiner/gametext.py +7 -4
  4. GameSentenceMiner/gsm.py +49 -10
  5. GameSentenceMiner/locales/en_us.json +7 -3
  6. GameSentenceMiner/locales/ja_jp.json +8 -4
  7. GameSentenceMiner/locales/zh_cn.json +8 -4
  8. GameSentenceMiner/obs.py +238 -59
  9. GameSentenceMiner/ocr/owocr_helper.py +1 -1
  10. GameSentenceMiner/tools/ss_selector.py +7 -8
  11. GameSentenceMiner/ui/__init__.py +0 -0
  12. GameSentenceMiner/ui/anki_confirmation.py +187 -0
  13. GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
  14. GameSentenceMiner/ui/screenshot_selector.py +215 -0
  15. GameSentenceMiner/util/configuration.py +124 -22
  16. GameSentenceMiner/util/db.py +22 -13
  17. GameSentenceMiner/util/downloader/download_tools.py +2 -2
  18. GameSentenceMiner/util/ffmpeg.py +24 -30
  19. GameSentenceMiner/util/get_overlay_coords.py +34 -34
  20. GameSentenceMiner/util/gsm_utils.py +31 -1
  21. GameSentenceMiner/util/text_log.py +11 -9
  22. GameSentenceMiner/vad.py +31 -12
  23. GameSentenceMiner/web/database_api.py +742 -123
  24. GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
  25. GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
  26. GameSentenceMiner/web/static/css/overview.css +850 -0
  27. GameSentenceMiner/web/static/css/popups-shared.css +126 -0
  28. GameSentenceMiner/web/static/css/shared.css +97 -0
  29. GameSentenceMiner/web/static/css/stats.css +192 -597
  30. GameSentenceMiner/web/static/js/anki_stats.js +6 -4
  31. GameSentenceMiner/web/static/js/database.js +209 -5
  32. GameSentenceMiner/web/static/js/goals.js +610 -0
  33. GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
  34. GameSentenceMiner/web/static/js/overview.js +1176 -0
  35. GameSentenceMiner/web/static/js/shared.js +25 -0
  36. GameSentenceMiner/web/static/js/stats.js +154 -1459
  37. GameSentenceMiner/web/stats.py +2 -2
  38. GameSentenceMiner/web/templates/anki_stats.html +5 -0
  39. GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
  40. GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
  41. GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
  42. GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
  43. GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
  44. GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
  45. GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
  46. GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
  47. GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
  48. GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
  49. GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
  50. GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
  51. GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
  52. GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
  53. GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
  54. GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
  55. GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
  56. GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
  57. GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
  58. GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
  59. GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
  60. GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
  61. GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
  62. GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
  63. GameSentenceMiner/web/templates/components/navigation.html +3 -1
  64. GameSentenceMiner/web/templates/database.html +73 -1
  65. GameSentenceMiner/web/templates/goals.html +376 -0
  66. GameSentenceMiner/web/templates/index.html +13 -11
  67. GameSentenceMiner/web/templates/overview.html +416 -0
  68. GameSentenceMiner/web/templates/stats.html +46 -251
  69. GameSentenceMiner/web/texthooking_page.py +18 -0
  70. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
  71. gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
  72. gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
  73. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
  74. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
  75. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
  76. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,610 @@
1
+ // Goals Page JavaScript
2
+ // Dependencies: shared.js (provides utility functions like showElement, hideElement, escapeHtml)
3
+
4
+ document.addEventListener('DOMContentLoaded', function () {
5
+
6
+ // Helper function to format large numbers
7
+ function formatGoalNumber(num) {
8
+ if (num >= 1000000) {
9
+ return (num / 1000000).toFixed(1) + 'M';
10
+ } else if (num >= 1000) {
11
+ return (num / 1000).toFixed(1) + 'K';
12
+ }
13
+ return num.toLocaleString();
14
+ }
15
+
16
+ // Helper function to format hours
17
+ function formatHours(hours) {
18
+ if (hours < 1) {
19
+ const minutes = Math.round(hours * 60);
20
+ return `${minutes}m`;
21
+ } else {
22
+ const h = Math.floor(hours);
23
+ const m = Math.round((hours - h) * 60);
24
+ return h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
25
+ }
26
+ }
27
+
28
+ // Function to update progress bar color based on percentage
29
+ function updateProgressBarColor(progressElement, percentage) {
30
+ progressElement.classList.remove('completion-0', 'completion-25', 'completion-50', 'completion-75', 'completion-100');
31
+
32
+ if (percentage >= 100) {
33
+ progressElement.classList.add('completion-100');
34
+ } else if (percentage >= 75) {
35
+ progressElement.classList.add('completion-75');
36
+ } else if (percentage >= 50) {
37
+ progressElement.classList.add('completion-50');
38
+ } else if (percentage >= 25) {
39
+ progressElement.classList.add('completion-25');
40
+ } else {
41
+ progressElement.classList.add('completion-0');
42
+ }
43
+ }
44
+
45
+ // Function to load goal progress chart
46
+ async function loadGoalProgress() {
47
+ const goalProgressChart = document.getElementById('goalProgressChart');
48
+ const goalProgressLoading = document.getElementById('goalProgressLoading');
49
+ const goalProgressError = document.getElementById('goalProgressError');
50
+
51
+ if (!goalProgressChart) return;
52
+
53
+ try {
54
+ goalProgressLoading.style.display = 'flex';
55
+ goalProgressError.style.display = 'none';
56
+
57
+ const response = await fetch('/api/stats');
58
+ if (!response.ok) throw new Error('Failed to fetch stats data');
59
+
60
+ const data = await response.json();
61
+ const allGamesStats = data.allGamesStats;
62
+ const allLinesData = data.allLinesData || [];
63
+
64
+ if (!allGamesStats) {
65
+ throw new Error('No stats data available');
66
+ }
67
+
68
+ // Get goal settings
69
+ const goalSettings = window.statsConfig || {};
70
+ const hoursTarget = goalSettings.readingHoursTarget || 1500;
71
+ const charsTarget = goalSettings.characterCountTarget || 25000000;
72
+ const gamesTarget = goalSettings.gamesTarget || 100;
73
+
74
+ // Calculate current progress
75
+ const currentHours = allGamesStats.total_time_hours || 0;
76
+ const currentCharacters = allGamesStats.total_characters || 0;
77
+ const currentGames = allGamesStats.unique_games || 0;
78
+
79
+ // Calculate daily averages for projections using 90-day lookback period (reusing logic from stats.js)
80
+ const dailyHoursAvg = calculateDailyAverage(allLinesData, 'hours');
81
+ const dailyCharsAvg = calculateDailyAverage(allLinesData, 'characters');
82
+ const dailyGamesAvg = calculateDailyAverage(allLinesData, 'games');
83
+
84
+ // Update Hours Goal
85
+ const hoursPercentage = Math.min(100, (currentHours / hoursTarget) * 100);
86
+ document.getElementById('goalHoursCurrent').textContent = Math.floor(currentHours).toLocaleString();
87
+ document.getElementById('goalHoursTarget').textContent = hoursTarget.toLocaleString();
88
+ document.getElementById('goalHoursPercentage').textContent = Math.floor(hoursPercentage) + '%';
89
+ document.getElementById('goalHoursProjection').textContent =
90
+ formatProjection(currentHours, hoursTarget, dailyHoursAvg);
91
+
92
+ const hoursProgressBar = document.getElementById('goalHoursProgress');
93
+ hoursProgressBar.style.width = hoursPercentage + '%';
94
+ hoursProgressBar.setAttribute('data-percentage', Math.floor(hoursPercentage / 25) * 25);
95
+ updateProgressBarColor(hoursProgressBar, hoursPercentage);
96
+
97
+ // Update Characters Goal
98
+ const charsPercentage = Math.min(100, (currentCharacters / charsTarget) * 100);
99
+ document.getElementById('goalCharsCurrent').textContent = formatGoalNumber(currentCharacters);
100
+ document.getElementById('goalCharsTarget').textContent = formatGoalNumber(charsTarget);
101
+ document.getElementById('goalCharsPercentage').textContent = Math.floor(charsPercentage) + '%';
102
+ document.getElementById('goalCharsProjection').textContent =
103
+ formatProjection(currentCharacters, charsTarget, dailyCharsAvg);
104
+
105
+ const charsProgressBar = document.getElementById('goalCharsProgress');
106
+ charsProgressBar.style.width = charsPercentage + '%';
107
+ charsProgressBar.setAttribute('data-percentage', Math.floor(charsPercentage / 25) * 25);
108
+ updateProgressBarColor(charsProgressBar, charsPercentage);
109
+
110
+ // Update Games Goal
111
+ const gamesPercentage = Math.min(100, (currentGames / gamesTarget) * 100);
112
+ document.getElementById('goalGamesCurrent').textContent = currentGames.toLocaleString();
113
+ document.getElementById('goalGamesTarget').textContent = gamesTarget.toLocaleString();
114
+ document.getElementById('goalGamesPercentage').textContent = Math.floor(gamesPercentage) + '%';
115
+ document.getElementById('goalGamesProjection').textContent =
116
+ formatProjection(currentGames, gamesTarget, dailyGamesAvg);
117
+
118
+ const gamesProgressBar = document.getElementById('goalGamesProgress');
119
+ gamesProgressBar.style.width = gamesPercentage + '%';
120
+ gamesProgressBar.setAttribute('data-percentage', Math.floor(gamesPercentage / 25) * 25);
121
+ updateProgressBarColor(gamesProgressBar, gamesPercentage);
122
+
123
+ goalProgressLoading.style.display = 'none';
124
+
125
+ } catch (error) {
126
+ console.error('Error loading goal progress:', error);
127
+ goalProgressLoading.style.display = 'none';
128
+ goalProgressError.style.display = 'block';
129
+ }
130
+ }
131
+
132
+ // Function to calculate daily average using a 90-day lookback period (copied from stats.js)
133
+ function calculateDailyAverage(allLinesData, metricType) {
134
+ if (!allLinesData || allLinesData.length === 0) {
135
+ return 0;
136
+ }
137
+
138
+ const today = new Date();
139
+ const ninetyDaysAgo = new Date(today.getTime() - (90 * 24 * 60 * 60 * 1000));
140
+
141
+ const recentData = allLinesData.filter(line => {
142
+ const lineDate = new Date(line.timestamp * 1000);
143
+ return lineDate >= ninetyDaysAgo && lineDate <= today;
144
+ });
145
+
146
+ if (recentData.length === 0) {
147
+ return 0;
148
+ }
149
+
150
+ let dailyTotals = {};
151
+
152
+ if (metricType === 'hours') {
153
+ const dailyTimestamps = {};
154
+ for (const line of recentData) {
155
+ const ts = parseFloat(line.timestamp);
156
+ if (isNaN(ts)) continue;
157
+ const dateObj = new Date(ts * 1000);
158
+ const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
159
+ if (!dailyTimestamps[dateStr]) {
160
+ dailyTimestamps[dateStr] = [];
161
+ }
162
+ dailyTimestamps[dateStr].push(ts);
163
+ }
164
+
165
+ for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
166
+ if (timestamps.length >= 2) {
167
+ timestamps.sort((a, b) => a - b);
168
+ let dayHours = 0;
169
+ const afkTimerSeconds = 120; // Default AFK timer
170
+ for (let i = 1; i < timestamps.length; i++) {
171
+ const gap = timestamps[i] - timestamps[i-1];
172
+ dayHours += Math.min(gap, afkTimerSeconds) / 3600;
173
+ }
174
+ dailyTotals[dateStr] = dayHours;
175
+ } else if (timestamps.length === 1) {
176
+ dailyTotals[dateStr] = 1 / 3600;
177
+ }
178
+ }
179
+ } else if (metricType === 'characters') {
180
+ for (const line of recentData) {
181
+ const ts = parseFloat(line.timestamp);
182
+ if (isNaN(ts)) continue;
183
+ const dateObj = new Date(ts * 1000);
184
+ const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
185
+ dailyTotals[dateStr] = (dailyTotals[dateStr] || 0) + (line.characters || 0);
186
+ }
187
+ } else if (metricType === 'games') {
188
+ const dailyGames = {};
189
+ for (const line of recentData) {
190
+ const ts = parseFloat(line.timestamp);
191
+ if (isNaN(ts)) continue;
192
+ const dateObj = new Date(ts * 1000);
193
+ const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
194
+ if (!dailyGames[dateStr]) {
195
+ dailyGames[dateStr] = new Set();
196
+ }
197
+ dailyGames[dateStr].add(line.game_name);
198
+ }
199
+
200
+ for (const [dateStr, gamesSet] of Object.entries(dailyGames)) {
201
+ dailyTotals[dateStr] = gamesSet.size;
202
+ }
203
+ }
204
+
205
+ const totalDays = Object.keys(dailyTotals).length;
206
+ if (totalDays === 0) {
207
+ return 0;
208
+ }
209
+
210
+ const totalValue = Object.values(dailyTotals).reduce((sum, value) => sum + value, 0);
211
+ return totalValue / totalDays;
212
+ }
213
+
214
+ // Function to format projection text
215
+ function formatProjection(currentValue, targetValue, dailyAverage) {
216
+ if (currentValue >= targetValue) {
217
+ return 'Goal achieved! 🎉';
218
+ }
219
+
220
+ if (dailyAverage <= 0) {
221
+ return 'No recent activity';
222
+ }
223
+
224
+ const remaining = targetValue - currentValue;
225
+ const daysToComplete = Math.ceil(remaining / dailyAverage);
226
+
227
+ if (daysToComplete <= 0) {
228
+ return 'Goal achieved! 🎉';
229
+ } else if (daysToComplete === 1) {
230
+ return '~1 day remaining';
231
+ } else if (daysToComplete <= 7) {
232
+ return `~${daysToComplete} days remaining`;
233
+ } else if (daysToComplete <= 30) {
234
+ const weeks = Math.ceil(daysToComplete / 7);
235
+ return `~${weeks} week${weeks > 1 ? 's' : ''} remaining`;
236
+ } else if (daysToComplete <= 365) {
237
+ const months = Math.ceil(daysToComplete / 30);
238
+ return `~${months} month${months > 1 ? 's' : ''} remaining`;
239
+ } else {
240
+ const years = Math.ceil(daysToComplete / 365);
241
+ return `~${years} year${years > 1 ? 's' : ''} remaining`;
242
+ }
243
+ }
244
+
245
+ // Function to load today's goals
246
+ async function loadTodayGoals() {
247
+ try {
248
+ const response = await fetch('/api/goals-today');
249
+ if (!response.ok) throw new Error('Failed to fetch today goals');
250
+
251
+ const data = await response.json();
252
+ const today = new Date();
253
+ const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
254
+ document.getElementById('todayGoalsDate').textContent = dateStr;
255
+
256
+ let hasAnyTarget = false;
257
+
258
+ // Update hours goal
259
+ const hoursGoalItem = document.getElementById('hoursGoalItem');
260
+ if (data.hours && data.hours.has_target && !data.hours.expired) {
261
+ hasAnyTarget = true;
262
+ hoursGoalItem.style.display = 'block';
263
+ document.getElementById('hoursDaysRemaining').style.display = 'block';
264
+
265
+ document.getElementById('todayHoursProgress').textContent = formatHours(data.hours.progress);
266
+ document.getElementById('todayHoursRequired').textContent = formatHours(data.hours.required);
267
+ document.getElementById('hoursRemainingValue').textContent = data.hours.days_remaining;
268
+
269
+ // Add green highlight if goal is met
270
+ if (data.hours.progress >= data.hours.required) {
271
+ hoursGoalItem.classList.add('goal-met');
272
+ } else {
273
+ hoursGoalItem.classList.remove('goal-met');
274
+ }
275
+ } else {
276
+ hoursGoalItem.style.display = 'none';
277
+ document.getElementById('hoursDaysRemaining').style.display = 'none';
278
+ }
279
+
280
+ // Update characters goal
281
+ const charsGoalItem = document.getElementById('charsGoalItem');
282
+ if (data.characters && data.characters.has_target && !data.characters.expired) {
283
+ hasAnyTarget = true;
284
+ charsGoalItem.style.display = 'block';
285
+ document.getElementById('charsDaysRemaining').style.display = 'block';
286
+
287
+ document.getElementById('todayCharsProgress').textContent = formatGoalNumber(data.characters.progress);
288
+ document.getElementById('todayCharsRequired').textContent = formatGoalNumber(data.characters.required);
289
+ document.getElementById('charsRemainingValue').textContent = data.characters.days_remaining;
290
+
291
+ // Add green highlight if goal is met
292
+ if (data.characters.progress >= data.characters.required) {
293
+ charsGoalItem.classList.add('goal-met');
294
+ } else {
295
+ charsGoalItem.classList.remove('goal-met');
296
+ }
297
+ } else {
298
+ charsGoalItem.style.display = 'none';
299
+ document.getElementById('charsDaysRemaining').style.display = 'none';
300
+ }
301
+
302
+ // Show/hide sections based on whether any targets are set
303
+ if (hasAnyTarget) {
304
+ document.getElementById('noTargetsMessage').style.display = 'none';
305
+ document.getElementById('todayGoalsStats').style.display = 'grid';
306
+ document.getElementById('todayGoalsProgress').style.display = 'block';
307
+ } else {
308
+ document.getElementById('noTargetsMessage').style.display = 'block';
309
+ document.getElementById('todayGoalsStats').style.display = 'none';
310
+ document.getElementById('todayGoalsProgress').style.display = 'none';
311
+ }
312
+
313
+ } catch (error) {
314
+ console.error('Error loading today goals:', error);
315
+ }
316
+ }
317
+
318
+ // Function to load goal projections
319
+ async function loadGoalProjections() {
320
+ try {
321
+ const response = await fetch('/api/goals-projection');
322
+ if (!response.ok) throw new Error('Failed to fetch goal projections');
323
+
324
+ const data = await response.json();
325
+ let hasAnyProjection = false;
326
+
327
+ // Update hours projection
328
+ if (data.hours && data.hours.target_date) {
329
+ hasAnyProjection = true;
330
+ document.getElementById('hoursProjectionItem').style.display = 'block';
331
+ document.getElementById('hoursProjectionSummary').style.display = 'block';
332
+
333
+ document.getElementById('projectionHoursValue').textContent =
334
+ Math.floor(data.hours.projection).toLocaleString() + 'h';
335
+
336
+ // Calculate percentage difference
337
+ const hoursPercentDiff = ((data.hours.projection - data.hours.target) / data.hours.target) * 100;
338
+
339
+ // Status message with pace badge
340
+ const hoursStatus = document.getElementById('hoursProjectionStatus');
341
+ if (hoursPercentDiff >= 5) {
342
+ // Over-achieving by 5% or more
343
+ const badge = `<span class="pace-badge pace-ahead">+${Math.floor(hoursPercentDiff)}%</span>`;
344
+ hoursStatus.innerHTML = `On Track! 🎉 ${badge}`;
345
+ hoursStatus.className = 'dashboard-progress-value positive';
346
+ } else if (hoursPercentDiff >= -5) {
347
+ // Within ±5% - perfect pace
348
+ const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(hoursPercentDiff))}%</span>`;
349
+ hoursStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
350
+ hoursStatus.className = 'dashboard-progress-value positive';
351
+ } else if (hoursPercentDiff >= -15) {
352
+ // Slightly behind (-5% to -15%)
353
+ const shortfall = data.hours.target - data.hours.projection;
354
+ const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(hoursPercentDiff)}%</span>`;
355
+ hoursStatus.innerHTML = `${Math.floor(shortfall)}h short ${badge}`;
356
+ hoursStatus.className = 'dashboard-progress-value';
357
+ hoursStatus.style.color = 'var(--warning-color)';
358
+ } else {
359
+ // Significantly behind (< -15%)
360
+ const shortfall = data.hours.target - data.hours.projection;
361
+ const badge = `<span class="pace-badge pace-behind">${Math.floor(hoursPercentDiff)}%</span>`;
362
+ hoursStatus.innerHTML = `${Math.floor(shortfall)}h short ${badge}`;
363
+ hoursStatus.className = 'dashboard-progress-value';
364
+ hoursStatus.style.color = 'var(--danger-color)';
365
+ }
366
+ } else {
367
+ document.getElementById('hoursProjectionItem').style.display = 'none';
368
+ document.getElementById('hoursProjectionSummary').style.display = 'none';
369
+ }
370
+
371
+ // Update characters projection
372
+ if (data.characters && data.characters.target_date) {
373
+ hasAnyProjection = true;
374
+ document.getElementById('charsProjectionItem').style.display = 'block';
375
+ document.getElementById('charsProjectionSummary').style.display = 'block';
376
+
377
+ document.getElementById('projectionCharsValue').textContent = formatGoalNumber(data.characters.projection);
378
+
379
+ // Calculate percentage difference
380
+ const charsPercentDiff = ((data.characters.projection - data.characters.target) / data.characters.target) * 100;
381
+
382
+ // Status message with pace badge
383
+ const charsStatus = document.getElementById('charsProjectionStatus');
384
+ if (charsPercentDiff >= 5) {
385
+ // Over-achieving by 5% or more
386
+ const badge = `<span class="pace-badge pace-ahead">+${Math.floor(charsPercentDiff)}%</span>`;
387
+ charsStatus.innerHTML = `On Track! 🎉 ${badge}`;
388
+ charsStatus.className = 'dashboard-progress-value positive';
389
+ } else if (charsPercentDiff >= -5) {
390
+ // Within ±5% - perfect pace
391
+ const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(charsPercentDiff))}%</span>`;
392
+ charsStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
393
+ charsStatus.className = 'dashboard-progress-value positive';
394
+ } else if (charsPercentDiff >= -15) {
395
+ // Slightly behind (-5% to -15%)
396
+ const shortfall = data.characters.target - data.characters.projection;
397
+ const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(charsPercentDiff)}%</span>`;
398
+ charsStatus.innerHTML = `${formatGoalNumber(shortfall)} short ${badge}`;
399
+ charsStatus.className = 'dashboard-progress-value';
400
+ charsStatus.style.color = 'var(--warning-color)';
401
+ } else {
402
+ // Significantly behind (< -15%)
403
+ const shortfall = data.characters.target - data.characters.projection;
404
+ const badge = `<span class="pace-badge pace-behind">${Math.floor(charsPercentDiff)}%</span>`;
405
+ charsStatus.innerHTML = `${formatGoalNumber(shortfall)} short ${badge}`;
406
+ charsStatus.className = 'dashboard-progress-value';
407
+ charsStatus.style.color = 'var(--danger-color)';
408
+ }
409
+ } else {
410
+ document.getElementById('charsProjectionItem').style.display = 'none';
411
+ document.getElementById('charsProjectionSummary').style.display = 'none';
412
+ }
413
+
414
+ // Update games projection
415
+ if (data.games && data.games.target_date) {
416
+ hasAnyProjection = true;
417
+ document.getElementById('gamesProjectionItem').style.display = 'block';
418
+ document.getElementById('gamesProjectionSummary').style.display = 'block';
419
+
420
+ document.getElementById('projectionGamesValue').textContent = data.games.projection.toLocaleString();
421
+
422
+ // Calculate percentage difference
423
+ const gamesPercentDiff = ((data.games.projection - data.games.target) / data.games.target) * 100;
424
+
425
+ // Status message with pace badge
426
+ const gamesStatus = document.getElementById('gamesProjectionStatus');
427
+ if (gamesPercentDiff >= 5) {
428
+ // Over-achieving by 5% or more
429
+ const badge = `<span class="pace-badge pace-ahead">+${Math.floor(gamesPercentDiff)}%</span>`;
430
+ gamesStatus.innerHTML = `On Track! 🎉 ${badge}`;
431
+ gamesStatus.className = 'dashboard-progress-value positive';
432
+ } else if (gamesPercentDiff >= -5) {
433
+ // Within ±5% - perfect pace
434
+ const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(gamesPercentDiff))}%</span>`;
435
+ gamesStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
436
+ gamesStatus.className = 'dashboard-progress-value positive';
437
+ } else if (gamesPercentDiff >= -15) {
438
+ // Slightly behind (-5% to -15%)
439
+ const shortfall = data.games.target - data.games.projection;
440
+ const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(gamesPercentDiff)}%</span>`;
441
+ gamesStatus.innerHTML = `${shortfall} short ${badge}`;
442
+ gamesStatus.className = 'dashboard-progress-value';
443
+ gamesStatus.style.color = 'var(--warning-color)';
444
+ } else {
445
+ // Significantly behind (< -15%)
446
+ const shortfall = data.games.target - data.games.projection;
447
+ const badge = `<span class="pace-badge pace-behind">${Math.floor(gamesPercentDiff)}%</span>`;
448
+ gamesStatus.innerHTML = `${shortfall} short ${badge}`;
449
+ gamesStatus.className = 'dashboard-progress-value';
450
+ gamesStatus.style.color = 'var(--danger-color)';
451
+ }
452
+ } else {
453
+ document.getElementById('gamesProjectionItem').style.display = 'none';
454
+ document.getElementById('gamesProjectionSummary').style.display = 'none';
455
+ }
456
+
457
+ // Show/hide sections
458
+ if (hasAnyProjection) {
459
+ document.getElementById('noProjectionsMessage').style.display = 'none';
460
+ document.getElementById('projectionStats').style.display = 'grid';
461
+ document.getElementById('projectionProgress').style.display = 'block';
462
+ } else {
463
+ document.getElementById('noProjectionsMessage').style.display = 'block';
464
+ document.getElementById('projectionStats').style.display = 'none';
465
+ document.getElementById('projectionProgress').style.display = 'none';
466
+ }
467
+
468
+ } catch (error) {
469
+ console.error('Error loading goal projections:', error);
470
+ }
471
+ }
472
+
473
+ // Load initial data
474
+ loadGoalProgress();
475
+ loadTodayGoals();
476
+ loadGoalProjections();
477
+
478
+ // Settings modal functionality
479
+ const settingsModal = document.getElementById('settingsModal');
480
+ const settingsToggle = document.getElementById('settingsToggle');
481
+ const closeSettingsModal = document.getElementById('closeSettingsModal');
482
+ const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
483
+ const saveSettingsBtn = document.getElementById('saveSettingsBtn');
484
+ const settingsForm = document.getElementById('settingsForm');
485
+
486
+ // Populate settings modal with current values
487
+ if (window.statsConfig) {
488
+ const hoursTargetInput = document.getElementById('readingHoursTarget');
489
+ if (hoursTargetInput) hoursTargetInput.value = window.statsConfig.readingHoursTarget || 1500;
490
+
491
+ const charsTargetInput = document.getElementById('characterCountTarget');
492
+ if (charsTargetInput) charsTargetInput.value = window.statsConfig.characterCountTarget || 25000000;
493
+
494
+ const gamesTargetInput = document.getElementById('gamesTarget');
495
+ if (gamesTargetInput) gamesTargetInput.value = window.statsConfig.gamesTarget || 100;
496
+
497
+ const hoursDateInput = document.getElementById('readingHoursTargetDate');
498
+ if (hoursDateInput) hoursDateInput.value = window.statsConfig.readingHoursTargetDate || '';
499
+
500
+ const charsDateInput = document.getElementById('characterCountTargetDate');
501
+ if (charsDateInput) charsDateInput.value = window.statsConfig.characterCountTargetDate || '';
502
+
503
+ const gamesDateInput = document.getElementById('gamesTargetDate');
504
+ if (gamesDateInput) gamesDateInput.value = window.statsConfig.gamesTargetDate || '';
505
+ }
506
+
507
+ // Open settings modal
508
+ if (settingsToggle) {
509
+ settingsToggle.addEventListener('click', function() {
510
+ settingsModal.style.display = 'flex';
511
+ settingsModal.classList.add('show');
512
+ });
513
+ }
514
+
515
+ // Close settings modal
516
+ function closeModal() {
517
+ settingsModal.style.display = 'none';
518
+ settingsModal.classList.remove('show');
519
+ }
520
+
521
+ if (closeSettingsModal) {
522
+ closeSettingsModal.addEventListener('click', closeModal);
523
+ }
524
+
525
+ if (cancelSettingsBtn) {
526
+ cancelSettingsBtn.addEventListener('click', closeModal);
527
+ }
528
+
529
+ // Close modal when clicking outside
530
+ settingsModal.addEventListener('click', function(e) {
531
+ if (e.target === settingsModal) {
532
+ closeModal();
533
+ }
534
+ });
535
+
536
+ // Save settings
537
+ if (saveSettingsBtn) {
538
+ saveSettingsBtn.addEventListener('click', async function() {
539
+ try {
540
+ const formData = {
541
+ reading_hours_target: parseInt(document.getElementById('readingHoursTarget').value),
542
+ character_count_target: parseInt(document.getElementById('characterCountTarget').value),
543
+ games_target: parseInt(document.getElementById('gamesTarget').value),
544
+ reading_hours_target_date: document.getElementById('readingHoursTargetDate').value || '',
545
+ character_count_target_date: document.getElementById('characterCountTargetDate').value || '',
546
+ games_target_date: document.getElementById('gamesTargetDate').value || ''
547
+ };
548
+
549
+ const response = await fetch('/api/settings', {
550
+ method: 'POST',
551
+ headers: {
552
+ 'Content-Type': 'application/json',
553
+ },
554
+ body: JSON.stringify(formData)
555
+ });
556
+
557
+ const result = await response.json();
558
+
559
+ if (response.ok) {
560
+ // Update global config
561
+ window.statsConfig = {
562
+ ...window.statsConfig,
563
+ readingHoursTarget: formData.reading_hours_target,
564
+ characterCountTarget: formData.character_count_target,
565
+ gamesTarget: formData.games_target,
566
+ readingHoursTargetDate: formData.reading_hours_target_date,
567
+ characterCountTargetDate: formData.character_count_target_date,
568
+ gamesTargetDate: formData.games_target_date
569
+ };
570
+
571
+ // Show success message
572
+ const successDiv = document.getElementById('settingsSuccess');
573
+ successDiv.textContent = 'Settings saved successfully!';
574
+ successDiv.style.display = 'block';
575
+ setTimeout(() => {
576
+ successDiv.style.display = 'none';
577
+ }, 3000);
578
+
579
+ // Reload all data
580
+ setTimeout(() => {
581
+ loadGoalProgress();
582
+ loadTodayGoals();
583
+ loadGoalProjections();
584
+ }, 500);
585
+
586
+ // Close modal after short delay
587
+ setTimeout(closeModal, 1500);
588
+ } else {
589
+ // Show error message
590
+ const errorDiv = document.getElementById('settingsError');
591
+ errorDiv.textContent = result.error || 'Failed to save settings';
592
+ errorDiv.style.display = 'block';
593
+ setTimeout(() => {
594
+ errorDiv.style.display = 'none';
595
+ }, 5000);
596
+ }
597
+ } catch (error) {
598
+ console.error('Error saving settings:', error);
599
+ const errorDiv = document.getElementById('settingsError');
600
+ errorDiv.textContent = 'Network error while saving settings';
601
+ errorDiv.style.display = 'block';
602
+ }
603
+ });
604
+ }
605
+
606
+ // Make functions globally available
607
+ window.loadGoalProgress = loadGoalProgress;
608
+ window.loadTodayGoals = loadTodayGoals;
609
+ window.loadGoalProjections = loadGoalProjections;
610
+ });