GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.0__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 (70) hide show
  1. GameSentenceMiner/__init__.py +39 -0
  2. GameSentenceMiner/anki.py +6 -3
  3. GameSentenceMiner/gametext.py +13 -2
  4. GameSentenceMiner/gsm.py +40 -3
  5. GameSentenceMiner/locales/en_us.json +4 -0
  6. GameSentenceMiner/locales/ja_jp.json +4 -0
  7. GameSentenceMiner/locales/zh_cn.json +4 -0
  8. GameSentenceMiner/obs.py +4 -1
  9. GameSentenceMiner/owocr/owocr/ocr.py +304 -134
  10. GameSentenceMiner/owocr/owocr/run.py +1 -1
  11. GameSentenceMiner/ui/anki_confirmation.py +4 -2
  12. GameSentenceMiner/ui/config_gui.py +12 -0
  13. GameSentenceMiner/util/configuration.py +6 -2
  14. GameSentenceMiner/util/cron/__init__.py +12 -0
  15. GameSentenceMiner/util/cron/daily_rollup.py +613 -0
  16. GameSentenceMiner/util/cron/jiten_update.py +397 -0
  17. GameSentenceMiner/util/cron/populate_games.py +154 -0
  18. GameSentenceMiner/util/cron/run_crons.py +148 -0
  19. GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
  20. GameSentenceMiner/util/cron_table.py +334 -0
  21. GameSentenceMiner/util/db.py +236 -49
  22. GameSentenceMiner/util/ffmpeg.py +23 -4
  23. GameSentenceMiner/util/games_table.py +340 -93
  24. GameSentenceMiner/util/jiten_api_client.py +188 -0
  25. GameSentenceMiner/util/stats_rollup_table.py +216 -0
  26. GameSentenceMiner/web/anki_api_endpoints.py +438 -220
  27. GameSentenceMiner/web/database_api.py +955 -1259
  28. GameSentenceMiner/web/jiten_database_api.py +1015 -0
  29. GameSentenceMiner/web/rollup_stats.py +672 -0
  30. GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
  31. GameSentenceMiner/web/static/css/overview.css +604 -47
  32. GameSentenceMiner/web/static/css/search.css +226 -0
  33. GameSentenceMiner/web/static/css/shared.css +762 -0
  34. GameSentenceMiner/web/static/css/stats.css +221 -0
  35. GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
  36. GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
  37. GameSentenceMiner/web/static/js/database-game-data.js +390 -0
  38. GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
  39. GameSentenceMiner/web/static/js/database-helpers.js +44 -0
  40. GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
  41. GameSentenceMiner/web/static/js/database-popups.js +89 -0
  42. GameSentenceMiner/web/static/js/database-tabs.js +64 -0
  43. GameSentenceMiner/web/static/js/database-text-management.js +371 -0
  44. GameSentenceMiner/web/static/js/database.js +86 -718
  45. GameSentenceMiner/web/static/js/goals.js +79 -18
  46. GameSentenceMiner/web/static/js/heatmap.js +29 -23
  47. GameSentenceMiner/web/static/js/overview.js +1205 -339
  48. GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
  49. GameSentenceMiner/web/static/js/search.js +215 -18
  50. GameSentenceMiner/web/static/js/shared.js +193 -39
  51. GameSentenceMiner/web/static/js/stats.js +1536 -179
  52. GameSentenceMiner/web/stats.py +1142 -269
  53. GameSentenceMiner/web/stats_api.py +2104 -0
  54. GameSentenceMiner/web/templates/anki_stats.html +4 -18
  55. GameSentenceMiner/web/templates/components/date-range.html +118 -3
  56. GameSentenceMiner/web/templates/components/html-head.html +40 -6
  57. GameSentenceMiner/web/templates/components/js-config.html +8 -8
  58. GameSentenceMiner/web/templates/components/regex-input.html +160 -0
  59. GameSentenceMiner/web/templates/database.html +564 -117
  60. GameSentenceMiner/web/templates/goals.html +41 -5
  61. GameSentenceMiner/web/templates/overview.html +159 -129
  62. GameSentenceMiner/web/templates/search.html +78 -9
  63. GameSentenceMiner/web/templates/stats.html +159 -5
  64. GameSentenceMiner/web/texthooking_page.py +280 -111
  65. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
  66. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
  67. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
  68. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
  69. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
  70. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
- // Database Management JavaScript
2
- // Dependencies: shared.js (provides utility functions like escapeHtml, openModal, closeModal)
1
+ // Database Management JavaScript - Main Entry Point
2
+ // Dependencies: All database modules must be loaded before this file
3
3
 
4
- // Database Management Class
4
+ /**
5
+ * Lightweight DatabaseManager class that orchestrates all database modules
6
+ */
5
7
  class DatabaseManager {
6
8
  constructor() {
7
9
  this.selectedGames = new Set();
@@ -9,28 +11,18 @@ class DatabaseManager {
9
11
  this.initializePage();
10
12
  }
11
13
 
14
+ /**
15
+ * Initialize the database management page
16
+ */
12
17
  async initializePage() {
13
18
  await this.loadDashboardStats();
14
19
  this.attachEventHandlers();
15
20
  }
16
21
 
22
+ /**
23
+ * Attach event handlers for all database functionality
24
+ */
17
25
  attachEventHandlers() {
18
- // Attach event handlers for buttons that were using onclick
19
- const openGameDeletionBtn = document.querySelector('[data-action="openGameDeletionModal"]');
20
- if (openGameDeletionBtn) {
21
- openGameDeletionBtn.addEventListener('click', openGameDeletionModal);
22
- }
23
-
24
- const openTextLinesBtn = document.querySelector('[data-action="openTextLinesModal"]');
25
- if (openTextLinesBtn) {
26
- openTextLinesBtn.addEventListener('click', openTextLinesModal);
27
- }
28
-
29
- const openDeduplicationBtn = document.querySelector('[data-action="openDeduplicationModal"]');
30
- if (openDeduplicationBtn) {
31
- openDeduplicationBtn.addEventListener('click', openDeduplicationModal);
32
- }
33
-
34
26
  // Modal close handlers
35
27
  const closeButtons = document.querySelectorAll('[data-action="closeModal"]');
36
28
  closeButtons.forEach(btn => {
@@ -40,743 +32,119 @@ class DatabaseManager {
40
32
  }
41
33
  });
42
34
 
43
- // Other action buttons
44
- const selectAllBtn = document.querySelector('[data-action="selectAllGames"]');
45
- if (selectAllBtn) {
46
- selectAllBtn.addEventListener('click', selectAllGames);
47
- }
48
-
49
- const selectNoneBtn = document.querySelector('[data-action="selectNoGames"]');
50
- if (selectNoneBtn) {
51
- selectNoneBtn.addEventListener('click', selectNoGames);
52
- }
53
-
54
- const deleteSelectedBtn = document.querySelector('[data-action="deleteSelectedGames"]');
55
- if (deleteSelectedBtn) {
56
- deleteSelectedBtn.addEventListener('click', deleteSelectedGames);
57
- }
58
-
59
- const mergeSelectedBtn = document.querySelector('[data-action="mergeSelectedGames"]');
60
- if (mergeSelectedBtn) {
61
- mergeSelectedBtn.addEventListener('click', openGameMergeModal);
35
+ // Initialize all module event handlers
36
+ if (typeof initializeTabHandlers === 'function') {
37
+ initializeTabHandlers();
62
38
  }
63
-
64
- const confirmMergeBtn = document.querySelector('[data-action="confirmGameMerge"]');
65
- if (confirmMergeBtn) {
66
- confirmMergeBtn.addEventListener('click', confirmGameMerge);
67
- }
68
-
69
- const presetPatternsSelect = document.getElementById('presetPatterns');
70
- if (presetPatternsSelect) {
71
- presetPatternsSelect.addEventListener('change', applyPresetPattern);
39
+
40
+ if (typeof initializeGameDataFilters === 'function') {
41
+ initializeGameDataFilters();
72
42
  }
73
-
74
- const previewDeleteBtn = document.querySelector('[data-action="previewTextDeletion"]');
75
- if (previewDeleteBtn) {
76
- previewDeleteBtn.addEventListener('click', previewTextDeletion);
43
+
44
+ if (typeof initializeBulkOperations === 'function') {
45
+ initializeBulkOperations();
77
46
  }
78
-
79
- const executeDeleteBtn = document.querySelector('[data-action="deleteTextLines"]');
80
- if (executeDeleteBtn) {
81
- executeDeleteBtn.addEventListener('click', deleteTextLines);
47
+
48
+ if (typeof initializeTextManagement === 'function') {
49
+ initializeTextManagement();
82
50
  }
83
-
84
- const scanDuplicatesBtn = document.querySelector('[data-action="scanForDuplicates"]');
85
- if (scanDuplicatesBtn) {
86
- scanDuplicatesBtn.addEventListener('click', scanForDuplicates);
51
+
52
+ if (typeof initializeJitenIntegration === 'function') {
53
+ initializeJitenIntegration();
87
54
  }
88
-
89
- const removeDuplicatesBtn = document.querySelector('[data-action="removeDuplicates"]');
90
- if (removeDuplicatesBtn) {
91
- removeDuplicatesBtn.addEventListener('click', removeDuplicates);
55
+
56
+ if (typeof initializeGameOperations === 'function') {
57
+ initializeGameOperations();
92
58
  }
93
-
94
- // Add event listener for the ignore time window checkbox
95
- const ignoreTimeWindowCheckbox = document.getElementById('ignoreTimeWindow');
96
- if (ignoreTimeWindowCheckbox) {
97
- ignoreTimeWindowCheckbox.addEventListener('change', toggleTimeWindowVisibility);
59
+
60
+ if (typeof initializeDatabasePopups === 'function') {
61
+ initializeDatabasePopups();
98
62
  }
99
63
  }
100
64
 
65
+ /**
66
+ * Load dashboard statistics
67
+ */
101
68
  async loadDashboardStats() {
102
69
  try {
70
+ // Load general stats
103
71
  const response = await fetch('/api/games-list');
104
72
  const data = await response.json();
105
73
 
106
74
  if (response.ok && data.games) {
107
75
  const totalGames = data.games.length;
108
76
  const totalSentences = data.games.reduce((sum, game) => sum + game.sentence_count, 0);
77
+ const totalCharacters = data.games.reduce((sum, game) => sum + game.total_characters, 0);
109
78
 
110
79
  document.getElementById('totalGamesCount').textContent = totalGames.toLocaleString();
111
80
  document.getElementById('totalSentencesCount').textContent = totalSentences.toLocaleString();
81
+ document.getElementById('totalCharactersCount').textContent = totalCharacters.toLocaleString();
112
82
  }
83
+
84
+ // Load game management stats
85
+ await this.loadGameManagementStats();
113
86
  } catch (error) {
114
87
  console.error('Error loading dashboard stats:', error);
115
88
  document.getElementById('totalGamesCount').textContent = 'Error';
116
89
  document.getElementById('totalSentencesCount').textContent = 'Error';
117
90
  }
118
91
  }
119
- }
120
-
121
- // Games Management Functions
122
- async function openGameDeletionModal() {
123
- openModal('gamesDeletionModal');
124
- await loadGamesForDeletion();
125
- }
126
92
 
127
- async function loadGamesForDeletion() {
128
- const loadingIndicator = document.getElementById('gamesLoadingIndicator');
129
- const content = document.getElementById('gamesContent');
130
- const gamesList = document.getElementById('gamesList');
131
-
132
- loadingIndicator.style.display = 'flex';
133
- content.style.display = 'none';
134
-
135
- try {
136
- const response = await fetch('/api/games-list');
137
- const data = await response.json();
138
-
139
- if (response.ok && data.games) {
140
- gamesList.innerHTML = '';
93
+ /**
94
+ * Load game management statistics
95
+ */
96
+ async loadGameManagementStats() {
97
+ try {
98
+ const gamesResponse = await fetch('/api/games-management');
99
+ const gamesData = await gamesResponse.json();
141
100
 
142
- data.games.forEach(game => {
143
- const gameItem = document.createElement('div');
144
- gameItem.className = 'checkbox-container';
145
- gameItem.innerHTML = `
146
- <input type="checkbox" class="checkbox-input game-checkbox" data-game="${escapeHtml(game.name)}">
147
- <label class="checkbox-label">
148
- <strong>${escapeHtml(game.name)}</strong><br>
149
- <small style="color: var(--text-tertiary);">
150
- ${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} characters
151
- </small>
152
- </label>
153
- `;
154
-
155
- // Add event listener for the checkbox
156
- const checkbox = gameItem.querySelector('.game-checkbox');
157
- checkbox.addEventListener('change', (event) => handleGameSelectionChange(event));
101
+ if (gamesResponse.ok && gamesData.summary) {
102
+ const linkedElement = document.getElementById('linkedGamesCount');
103
+ const unlinkedElement = document.getElementById('unlinkedGamesCount');
158
104
 
159
- gamesList.appendChild(gameItem);
160
- });
161
-
162
- content.style.display = 'block';
163
- }
164
- } catch (error) {
165
- console.error('Error loading games:', error);
166
- gamesList.innerHTML = '<p class="error-text">Failed to load games</p>';
167
- content.style.display = 'block';
168
- } finally {
169
- loadingIndicator.style.display = 'none';
170
- }
171
- }
172
-
173
- function selectAllGames() {
174
- // Clear current merge target
175
- databaseManager.mergeTargetGame = null;
176
- document.querySelectorAll('.checkbox-container').forEach(container => {
177
- container.classList.remove('merge-target');
178
- });
179
-
180
- const checkboxes = document.querySelectorAll('.game-checkbox');
181
- checkboxes.forEach((cb, index) => {
182
- cb.checked = true;
183
- // Mark the first checkbox as merge target
184
- if (index === 0) {
185
- databaseManager.mergeTargetGame = cb.dataset.game;
186
- cb.closest('.checkbox-container').classList.add('merge-target');
187
- }
188
- });
189
- updateGameSelection();
190
- }
191
-
192
- function selectNoGames() {
193
- // Clear merge target
194
- databaseManager.mergeTargetGame = null;
195
- document.querySelectorAll('.checkbox-container').forEach(container => {
196
- container.classList.remove('merge-target');
197
- });
198
-
199
- document.querySelectorAll('.game-checkbox').forEach(cb => {
200
- cb.checked = false;
201
- });
202
- updateGameSelection();
203
- }
204
-
205
- function handleGameSelectionChange(event) {
206
- const checkbox = event.target;
207
- const gameName = checkbox.dataset.game;
208
- const isChecked = checkbox.checked;
209
-
210
- // Get current selection count before updating
211
- const currentSelectedCount = document.querySelectorAll('.game-checkbox:checked').length - (isChecked ? 1 : 0);
212
-
213
- if (isChecked) {
214
- // Game is being selected
215
- if (currentSelectedCount === 0) {
216
- // This is the first game being selected, mark it as merge target
217
- databaseManager.mergeTargetGame = gameName;
218
- // Add visual indicator
219
- checkbox.closest('.checkbox-container').classList.add('merge-target');
220
- }
221
- } else {
222
- // Game is being deselected
223
- if (gameName === databaseManager.mergeTargetGame) {
224
- // The merge target is being deselected
225
- databaseManager.mergeTargetGame = null;
226
- checkbox.closest('.checkbox-container').classList.remove('merge-target');
227
-
228
- // If there are still other games selected, make the first one the new target
229
- const remainingSelected = document.querySelectorAll('.game-checkbox:checked');
230
- if (remainingSelected.length > 0) {
231
- const newTargetCheckbox = remainingSelected[0];
232
- const newTargetGame = newTargetCheckbox.dataset.game;
233
- databaseManager.mergeTargetGame = newTargetGame;
234
- newTargetCheckbox.closest('.checkbox-container').classList.add('merge-target');
235
- }
236
- } else {
237
- // Remove merge target styling if it exists
238
- checkbox.closest('.checkbox-container').classList.remove('merge-target');
239
- }
240
- }
241
-
242
- updateGameSelection();
243
- }
244
-
245
- function updateGameSelection() {
246
- const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
247
- const deleteBtn = document.getElementById('deleteSelectedGamesBtn');
248
- const mergeBtn = document.getElementById('mergeSelectedGamesBtn');
249
-
250
- // Update delete button
251
- deleteBtn.disabled = selectedCheckboxes.length === 0;
252
- deleteBtn.textContent = selectedCheckboxes.length > 0 ? `Delete Selected (${selectedCheckboxes.length})` : 'Delete Selected';
253
-
254
- // Update merge button - only enable when 2 or more games are selected
255
- mergeBtn.disabled = selectedCheckboxes.length < 2;
256
- mergeBtn.textContent = selectedCheckboxes.length >= 2 ? `Merge Selected (${selectedCheckboxes.length})` : 'Merge Selected Games';
257
- }
258
-
259
- async function deleteSelectedGames() {
260
- const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
261
- const gameNames = Array.from(selectedCheckboxes).map(cb => cb.dataset.game);
262
-
263
- if (gameNames.length === 0) return;
264
-
265
- if (!confirm(`Are you sure you want to delete ${gameNames.length} game(s)? This action cannot be undone.`)) {
266
- return;
267
- }
268
-
269
- try {
270
- const response = await fetch('/api/delete-games', {
271
- method: 'POST',
272
- headers: { 'Content-Type': 'application/json' },
273
- body: JSON.stringify({ game_names: gameNames })
274
- });
275
-
276
- const result = await response.json();
277
-
278
- if (response.ok) {
279
- alert(`Successfully deleted ${result.successful_games.length} games!`);
280
- closeModal('gamesDeletionModal');
281
- await databaseManager.loadDashboardStats();
282
- } else {
283
- alert(`Error: ${result.error}`);
284
- }
285
- } catch (error) {
286
- console.error('Error deleting games:', error);
287
- alert('Failed to delete games');
288
- }
289
- }
290
-
291
- // Text Lines Functions
292
- function openTextLinesModal() {
293
- openModal('textLinesModal');
294
- // Reset the modal state
295
- document.getElementById('presetPatterns').value = '';
296
- document.getElementById('customRegex').value = '';
297
- document.getElementById('textToDelete').value = '';
298
- document.getElementById('previewDeleteResults').style.display = 'none';
299
- document.getElementById('executeDeleteBtn').disabled = true;
300
- }
301
-
302
- // Preset pattern definitions
303
- const presetPatterns = {
304
- 'lines_over_50': '.{51,}',
305
- 'lines_over_100': '.{101,}',
306
- 'non_japanese': '^[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]*$',
307
- 'ascii_only': '^[\x00-\x7F]*$',
308
- 'empty_lines': '^\s*$',
309
- 'numbers_only': '^\d+$',
310
- 'single_char': '^.{1}$',
311
- 'repeated_chars': '(.)\\1{2,}'
312
- };
313
-
314
- function applyPresetPattern() {
315
- const selectedPattern = document.getElementById('presetPatterns').value;
316
- const customRegexInput = document.getElementById('customRegex');
317
- const useRegexCheckbox = document.getElementById('useRegexDelete');
318
-
319
- if (selectedPattern && presetPatterns[selectedPattern]) {
320
- customRegexInput.value = presetPatterns[selectedPattern];
321
- useRegexCheckbox.checked = true;
322
- // Clear preview when pattern changes
323
- document.getElementById('previewDeleteResults').style.display = 'none';
324
- document.getElementById('executeDeleteBtn').disabled = true;
325
- }
326
- }
327
-
328
- async function previewTextDeletion() {
329
- const customRegex = document.getElementById('customRegex').value;
330
- const textToDelete = document.getElementById('textToDelete').value;
331
- const caseSensitive = document.getElementById('caseSensitiveDelete').checked;
332
- const useRegex = document.getElementById('useRegexDelete').checked;
333
- const errorDiv = document.getElementById('textLinesError');
334
- const previewDiv = document.getElementById('previewDeleteResults');
335
-
336
- errorDiv.style.display = 'none';
337
- previewDiv.style.display = 'none';
338
-
339
- // Validate input
340
- if (!customRegex.trim() && !textToDelete.trim()) {
341
- errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
342
- errorDiv.style.display = 'block';
343
- return;
344
- }
345
-
346
- try {
347
- // Prepare request data
348
- const requestData = {
349
- regex_pattern: customRegex.trim() || null,
350
- exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
351
- case_sensitive: caseSensitive,
352
- use_regex: useRegex,
353
- preview_only: true
354
- };
355
-
356
- const response = await fetch('/api/preview-text-deletion', {
357
- method: 'POST',
358
- headers: { 'Content-Type': 'application/json' },
359
- body: JSON.stringify(requestData)
360
- });
361
-
362
- const result = await response.json();
363
-
364
- if (response.ok) {
365
- // Show preview results
366
- document.getElementById('previewDeleteCount').textContent = result.count.toLocaleString();
367
-
368
- const samplesDiv = document.getElementById('previewDeleteSamples');
369
- if (result.samples && result.samples.length > 0) {
370
- samplesDiv.innerHTML = '<strong>Sample matches:</strong><br>' +
371
- result.samples.slice(0, 5).map(sample =>
372
- `<div style="font-size: 12px; color: var(--text-tertiary); margin: 5px 0; padding: 5px; background: var(--bg-secondary); border-radius: 3px;">${escapeHtml(sample)}</div>`
373
- ).join('');
374
- } else {
375
- samplesDiv.innerHTML = '<em>No matches found</em>';
105
+ if (linkedElement) {
106
+ linkedElement.textContent = gamesData.summary.linked_games.toLocaleString();
107
+ }
108
+ if (unlinkedElement) {
109
+ unlinkedElement.textContent = gamesData.summary.unlinked_games.toLocaleString();
110
+ }
376
111
  }
377
-
378
- previewDiv.style.display = 'block';
379
- document.getElementById('executeDeleteBtn').disabled = result.count === 0;
380
- } else {
381
- errorDiv.textContent = result.error || 'Failed to preview deletion';
382
- errorDiv.style.display = 'block';
383
- }
384
- } catch (error) {
385
- console.error('Error previewing text deletion:', error);
386
- // For now, show a placeholder since backend isn't implemented yet
387
- errorDiv.textContent = 'Preview feature ready - backend endpoint needed';
388
- errorDiv.style.display = 'block';
389
- }
390
- }
391
-
392
- async function deleteTextLines() {
393
- const customRegex = document.getElementById('customRegex').value;
394
- const textToDelete = document.getElementById('textToDelete').value;
395
- const caseSensitive = document.getElementById('caseSensitiveDelete').checked;
396
- const useRegex = document.getElementById('useRegexDelete').checked;
397
- const errorDiv = document.getElementById('textLinesError');
398
- const successDiv = document.getElementById('textLinesSuccess');
399
-
400
- errorDiv.style.display = 'none';
401
- successDiv.style.display = 'none';
402
-
403
- if (!customRegex.trim() && !textToDelete.trim()) {
404
- errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
405
- errorDiv.style.display = 'block';
406
- return;
407
- }
408
-
409
- if (!confirm('This will permanently delete the selected text lines. Continue?')) {
410
- return;
411
- }
412
-
413
- try {
414
- const requestData = {
415
- regex_pattern: customRegex.trim() || null,
416
- exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
417
- case_sensitive: caseSensitive,
418
- use_regex: useRegex,
419
- preview_only: false
420
- };
421
-
422
- const response = await fetch('/api/delete-text-lines', {
423
- method: 'POST',
424
- headers: { 'Content-Type': 'application/json' },
425
- body: JSON.stringify(requestData)
426
- });
427
-
428
- const result = await response.json();
429
-
430
- if (response.ok) {
431
- successDiv.textContent = `Successfully deleted ${result.deleted_count} text lines!`;
432
- successDiv.style.display = 'block';
433
- // Refresh dashboard stats
434
- await databaseManager.loadDashboardStats();
435
- } else {
436
- errorDiv.textContent = result.error || 'Failed to delete text lines';
437
- errorDiv.style.display = 'block';
438
- }
439
- } catch (error) {
440
- console.error('Error deleting text lines:', error);
441
- // Placeholder for development
442
- successDiv.textContent = 'Text line deletion feature ready - backend endpoint needed';
443
- successDiv.style.display = 'block';
444
- }
445
- }
446
-
447
- // Deduplication Functions
448
- async function openDeduplicationModal() {
449
- openModal('deduplicationModal');
450
- await loadGamesForDeduplication();
451
- // Reset modal state
452
- document.getElementById('timeWindow').value = '5';
453
- document.getElementById('ignoreTimeWindow').checked = false;
454
- document.getElementById('deduplicationStats').style.display = 'none';
455
- document.getElementById('removeDuplicatesBtn').disabled = true;
456
- document.getElementById('deduplicationError').style.display = 'none';
457
- document.getElementById('deduplicationSuccess').style.display = 'none';
458
- // Ensure time window is visible on modal open
459
- toggleTimeWindowVisibility();
460
- }
461
-
462
- function toggleTimeWindowVisibility() {
463
- const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
464
- const timeWindowGroup = document.getElementById('timeWindowGroup');
465
-
466
- if (ignoreTimeWindow) {
467
- timeWindowGroup.style.opacity = '0.5';
468
- timeWindowGroup.style.pointerEvents = 'none';
469
- document.getElementById('timeWindow').disabled = true;
470
- } else {
471
- timeWindowGroup.style.opacity = '1';
472
- timeWindowGroup.style.pointerEvents = 'auto';
473
- document.getElementById('timeWindow').disabled = false;
474
- }
475
- }
476
-
477
- async function loadGamesForDeduplication() {
478
- try {
479
- const response = await fetch('/api/games-list');
480
- const data = await response.json();
481
-
482
- if (response.ok && data.games) {
483
- const gameSelect = document.getElementById('gameSelection');
484
- // Keep "All Games" option and add individual games
485
- gameSelect.innerHTML = '<option value="all">All Games</option>';
486
-
487
- data.games.forEach(game => {
488
- const option = document.createElement('option');
489
- option.value = game.name;
490
- option.textContent = `${game.name} (${game.sentence_count} sentences)`;
491
- gameSelect.appendChild(option);
492
- });
493
- }
494
- } catch (error) {
495
- console.error('Error loading games for deduplication:', error);
496
- }
497
- }
498
-
499
- async function scanForDuplicates() {
500
- const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
501
- const timeWindow = parseInt(document.getElementById('timeWindow').value);
502
- const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
503
- const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
504
- const statsDiv = document.getElementById('deduplicationStats');
505
- const errorDiv = document.getElementById('deduplicationError');
506
- const successDiv = document.getElementById('deduplicationSuccess');
507
- const removeBtn = document.getElementById('removeDuplicatesBtn');
508
-
509
- errorDiv.style.display = 'none';
510
- successDiv.style.display = 'none';
511
- statsDiv.style.display = 'none';
512
- removeBtn.disabled = true;
513
-
514
- // Validate input
515
- if (selectedGames.length === 0) {
516
- errorDiv.textContent = 'Please select at least one game';
517
- errorDiv.style.display = 'block';
518
- return;
519
- }
520
-
521
- // Only validate time window if not ignoring it
522
- if (!ignoreTimeWindow && (isNaN(timeWindow) || timeWindow < 1)) {
523
- errorDiv.textContent = 'Time window must be at least 1 minute';
524
- errorDiv.style.display = 'block';
525
- return;
526
- }
527
-
528
- try {
529
- const requestData = {
530
- games: selectedGames,
531
- time_window_minutes: timeWindow,
532
- case_sensitive: caseSensitive,
533
- ignore_time_window: ignoreTimeWindow,
534
- preview_only: true
535
- };
536
-
537
- const response = await fetch('/api/preview-deduplication', {
538
- method: 'POST',
539
- headers: { 'Content-Type': 'application/json' },
540
- body: JSON.stringify(requestData)
541
- });
542
-
543
- const result = await response.json();
544
-
545
- if (response.ok) {
546
- document.getElementById('duplicatesFoundCount').textContent = result.duplicates_count.toLocaleString();
547
- document.getElementById('gamesAffectedCount').textContent = result.games_affected.toString();
548
- document.getElementById('spaceToFree').textContent = `${result.duplicates_count} sentences`;
549
-
550
- // Show sample duplicates
551
- const samplesDiv = document.getElementById('duplicatesSampleList');
552
- if (result.samples && result.samples.length > 0) {
553
- samplesDiv.innerHTML = '<strong>Sample duplicates:</strong><br>' +
554
- result.samples.slice(0, 3).map(sample =>
555
- `<div style="font-size: 12px; color: var(--text-tertiary); margin: 5px 0; padding: 5px; background: var(--bg-secondary); border-radius: 3px;">${escapeHtml(sample.text)} (${sample.occurrences} times)</div>`
556
- ).join('');
557
- } else {
558
- samplesDiv.innerHTML = '<em>No duplicates found</em>';
559
- }
560
-
561
- statsDiv.style.display = 'block';
562
- removeBtn.disabled = result.duplicates_count === 0;
563
-
564
- if (result.duplicates_count > 0) {
565
- const modeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
566
- successDiv.textContent = `Found ${result.duplicates_count} duplicate sentences ${modeText} ready for removal.`;
567
- successDiv.style.display = 'block';
568
- } else {
569
- const modeText = ignoreTimeWindow ? 'across entire games' : 'within the specified time window';
570
- successDiv.textContent = `No duplicates found in the selected games ${modeText}.`;
571
- successDiv.style.display = 'block';
572
- }
573
- } else {
574
- errorDiv.textContent = result.error || 'Failed to scan for duplicates';
575
- errorDiv.style.display = 'block';
576
- }
577
- } catch (error) {
578
- console.error('Error scanning for duplicates:', error);
579
- // Placeholder for development
580
- const duplicatesFound = Math.floor(Math.random() * 50) + 5;
581
- document.getElementById('duplicatesFoundCount').textContent = duplicatesFound.toLocaleString();
582
- document.getElementById('gamesAffectedCount').textContent = Math.min(selectedGames.length, 3).toString();
583
- document.getElementById('spaceToFree').textContent = `${duplicatesFound} sentences`;
584
-
585
- statsDiv.style.display = 'block';
586
- removeBtn.disabled = false;
587
- const modeText = ignoreTimeWindow ? 'across entire games' : 'with time window';
588
- successDiv.textContent = `Preview feature ready - found ${duplicatesFound} potential duplicates ${modeText} (backend endpoint needed)`;
589
- successDiv.style.display = 'block';
590
- }
591
- }
592
-
593
- async function removeDuplicates() {
594
- const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
595
- const timeWindow = parseInt(document.getElementById('timeWindow').value);
596
- const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
597
- const preserveNewest = document.getElementById('preserveNewest').checked;
598
- const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
599
-
600
- const modeText = ignoreTimeWindow ? 'ALL duplicate sentences across entire games' : 'duplicate sentences within the time window';
601
- if (!confirm(`This will permanently remove ${modeText}. Continue?`)) {
602
- return;
603
- }
604
-
605
- try {
606
- const requestData = {
607
- games: selectedGames,
608
- time_window_minutes: timeWindow,
609
- case_sensitive: caseSensitive,
610
- preserve_newest: preserveNewest,
611
- ignore_time_window: ignoreTimeWindow,
612
- preview_only: false
613
- };
614
-
615
- const response = await fetch('/api/deduplicate', {
616
- method: 'POST',
617
- headers: { 'Content-Type': 'application/json' },
618
- body: JSON.stringify(requestData)
619
- });
620
-
621
- const result = await response.json();
622
-
623
- if (response.ok) {
624
- const successDiv = document.getElementById('deduplicationSuccess');
625
- const resultModeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
626
- successDiv.textContent = `Successfully removed ${result.deleted_count} duplicate sentences ${resultModeText}!`;
627
- successDiv.style.display = 'block';
628
- document.getElementById('removeDuplicatesBtn').disabled = true;
629
- // Refresh dashboard stats
630
- await databaseManager.loadDashboardStats();
631
- } else {
632
- const errorDiv = document.getElementById('deduplicationError');
633
- errorDiv.textContent = result.error || 'Failed to remove duplicates';
634
- errorDiv.style.display = 'block';
112
+ } catch (error) {
113
+ console.error('Error loading game management stats:', error);
114
+ const linkedElement = document.getElementById('linkedGamesCount');
115
+ const unlinkedElement = document.getElementById('unlinkedGamesCount');
116
+ if (linkedElement) linkedElement.textContent = 'Error';
117
+ if (unlinkedElement) unlinkedElement.textContent = 'Error';
635
118
  }
636
- } catch (error) {
637
- console.error('Error removing duplicates:', error);
638
- // Placeholder for development
639
- const successDiv = document.getElementById('deduplicationSuccess');
640
- successDiv.textContent = 'Deduplication feature ready - backend endpoint needed';
641
- successDiv.style.display = 'block';
642
- document.getElementById('removeDuplicatesBtn').disabled = true;
643
119
  }
644
120
  }
645
121
 
122
+ // Global database manager instance
123
+ let databaseManager;
646
124
 
647
- // Game Merge Functions
648
- async function openGameMergeModal() {
649
- const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
650
- const gameNames = Array.from(selectedCheckboxes).map(cb => cb.dataset.game);
651
-
652
- if (gameNames.length < 2) {
653
- alert('Please select at least 2 games to merge.');
654
- return;
655
- }
656
-
657
- try {
658
- // Get detailed game information
659
- const response = await fetch('/api/games-list');
660
- const data = await response.json();
661
-
662
- if (response.ok && data.games) {
663
- const selectedGames = data.games.filter(game => gameNames.includes(game.name));
664
-
665
- // Use the tracked merge target as primary game, or fall back to first selected
666
- let primaryGame = selectedGames.find(game => game.name === databaseManager.mergeTargetGame);
667
- if (!primaryGame) {
668
- primaryGame = selectedGames[0];
669
- }
670
-
671
- // Secondary games are all selected games except the primary
672
- const secondaryGames = selectedGames.filter(game => game.name !== primaryGame.name);
673
-
674
- // Calculate totals
675
- const totalSentences = selectedGames.reduce((sum, game) => sum + game.sentence_count, 0);
676
- const totalCharacters = selectedGames.reduce((sum, game) => sum + game.total_characters, 0);
677
-
678
- // Populate primary game info
679
- document.getElementById('primaryGameName').textContent = primaryGame.name;
680
- document.getElementById('primaryGameStats').textContent =
681
- `${primaryGame.sentence_count} sentences, ${primaryGame.total_characters.toLocaleString()} characters`;
682
-
683
- // Populate secondary games list
684
- const secondaryList = document.getElementById('secondaryGamesList');
685
- secondaryList.innerHTML = '';
686
- secondaryGames.forEach(game => {
687
- const gameDiv = document.createElement('div');
688
- gameDiv.className = 'game-item';
689
- gameDiv.innerHTML = `
690
- <div class="game-name">${escapeHtml(game.name)}</div>
691
- <div class="game-stats">${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} characters</div>
692
- `;
693
- secondaryList.appendChild(gameDiv);
694
- });
695
-
696
- // Update merge statistics
697
- document.getElementById('totalSentencesAfterMerge').textContent = totalSentences.toLocaleString();
698
- document.getElementById('totalCharactersAfterMerge').textContent = totalCharacters.toLocaleString();
699
- document.getElementById('gamesBeingMerged').textContent = gameNames.length;
700
-
701
- // Reset modal state
702
- document.getElementById('mergeError').style.display = 'none';
703
- document.getElementById('mergeSuccess').style.display = 'none';
704
- document.getElementById('mergeLoadingIndicator').style.display = 'none';
705
- document.getElementById('confirmMergeBtn').disabled = false;
706
-
707
- // Store selected games for the merge operation
708
- window.selectedGamesForMerge = gameNames;
709
-
710
- openModal('gameMergeModal');
711
- }
712
- } catch (error) {
713
- console.error('Error loading game data for merge:', error);
714
- alert('Failed to load game data for merge');
715
- }
716
- }
717
-
718
- async function confirmGameMerge() {
719
- const gameNames = window.selectedGamesForMerge;
720
-
721
- if (!gameNames || gameNames.length < 2) {
722
- alert('Invalid game selection for merge');
125
+ /**
126
+ * Initialize database management when DOM loads
127
+ */
128
+ document.addEventListener('DOMContentLoaded', function() {
129
+ // Ensure all required functions are available
130
+ const requiredFunctions = [
131
+ 'showDatabaseSuccessPopup',
132
+ 'showDatabaseErrorPopup',
133
+ 'showDatabaseConfirmPopup',
134
+ 'formatReleaseDate',
135
+ 'switchTab',
136
+ 'loadGamesForDataManagement'
137
+ ];
138
+
139
+ const missingFunctions = requiredFunctions.filter(fn => typeof window[fn] !== 'function');
140
+ if (missingFunctions.length > 0) {
141
+ console.error('Missing required functions:', missingFunctions);
142
+ console.error('Please ensure all database modules are loaded before database.js');
723
143
  return;
724
144
  }
725
145
 
726
- const errorDiv = document.getElementById('mergeError');
727
- const successDiv = document.getElementById('mergeSuccess');
728
- const loadingDiv = document.getElementById('mergeLoadingIndicator');
729
- const confirmBtn = document.getElementById('confirmMergeBtn');
730
-
731
- // Reset state
732
- errorDiv.style.display = 'none';
733
- successDiv.style.display = 'none';
734
-
735
- // Show loading state
736
- loadingDiv.style.display = 'flex';
737
- confirmBtn.disabled = true;
738
-
739
- try {
740
- target_game = databaseManager.mergeTargetGame || gameNames[0];
741
- const response = await fetch('/api/merge_games', {
742
- method: 'POST',
743
- headers: { 'Content-Type': 'application/json' },
744
- body: JSON.stringify(
745
- { target_game: target_game, games_to_merge: gameNames.filter(name => name !== target_game) })
746
- });
747
-
748
- const result = await response.json();
749
-
750
- if (response.ok) {
751
- // Show success message
752
- successDiv.textContent = `Successfully merged ${result.merged_games.length} games into "${result.primary_game}"! Moved ${result.lines_moved} sentences.`;
753
- successDiv.style.display = 'block';
754
-
755
- // Auto-close modal after 2 seconds and refresh
756
- setTimeout(async () => {
757
- closeModal('gameMergeModal');
758
- closeModal('gamesDeletionModal');
759
- await databaseManager.loadDashboardStats();
760
- }, 2000);
761
-
762
- } else {
763
- // Show error message
764
- errorDiv.textContent = result.error || 'Failed to merge games';
765
- errorDiv.style.display = 'block';
766
- confirmBtn.disabled = false;
767
- }
768
- } catch (error) {
769
- console.error('Error merging games:', error);
770
- errorDiv.textContent = 'Network error occurred while merging games';
771
- errorDiv.style.display = 'block';
772
- confirmBtn.disabled = false;
773
- } finally {
774
- loadingDiv.style.display = 'none';
775
- }
776
- }
777
-
778
- // Initialize page when DOM loads
779
- let databaseManager;
780
- document.addEventListener('DOMContentLoaded', function() {
146
+ // Initialize the database manager
781
147
  databaseManager = new DatabaseManager();
148
+
149
+ console.log('Database management system initialized successfully');
782
150
  });