diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt index 5d9dc07..e35e457 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt @@ -24,7 +24,11 @@ class SyncRepository(context: Context) { suspend fun performSync(): Result = withContext(Dispatchers.IO) { try { val lastSync = getLastSyncTimestamp() - Logger.d("Sync", "Starting sync with lastSyncTimestamp: $lastSync") + val isInitialSync = lastSync == 0L + + if (isInitialSync) { + Logger.i("Sync", "🔄 Starting initial sync with server") + } // Gather local changes since last sync val localLists = database.listDao().getListsUpdatedSince(lastSync) @@ -32,11 +36,19 @@ class SyncRepository(context: Context) { val localItems = database.itemDao().getItemsUpdatedSince(lastSync) val localValues = database.itemValueDao().getValuesUpdatedSince(lastSync) - Logger.i("Sync", "Sending to server: ${localLists.size} lists, ${localFields.size} fields, ${localItems.size} items") - - // Log details of lists being sent - localLists.forEach { list -> - Logger.d("Sync", " List: ${list.id} - ${list.name} (updated: ${list.updatedAt})") + // Only log when sending data + if (localLists.isNotEmpty() || localFields.isNotEmpty() || localItems.isNotEmpty()) { + Logger.i("Sync", "⬆️ Sending to server:") + if (localLists.isNotEmpty()) { + Logger.i("Sync", " 📋 ${localLists.size} list(s)") + localLists.forEach { list -> + val action = if (list.isDeleted) "Deleted" else "Updated" + Logger.i("Sync", " $action: ${list.name}") + } + } + if (localFields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${localFields.size} field(s)") + if (localItems.isNotEmpty()) Logger.i("Sync", " 📝 ${localItems.size} item(s)") + if (localValues.isNotEmpty()) Logger.i("Sync", " 💾 ${localValues.size} value(s)") } // Send to server and get updates @@ -53,44 +65,43 @@ class SyncRepository(context: Context) { if (response.isSuccessful) { val syncResponse = response.body()!! - Logger.i("Sync", "Received from server: ${syncResponse.lists.size} lists, ${syncResponse.fields.size} fields, ${syncResponse.items.size} items") - - // Log details of lists being received - syncResponse.lists.forEach { list -> - Logger.d("Sync", " Received list: ${list.id} - ${list.name} (updated: ${list.updatedAt}, deleted: ${list.isDeleted})") + // Only log when receiving data + if (syncResponse.lists.isNotEmpty() || syncResponse.fields.isNotEmpty() || + syncResponse.items.isNotEmpty() || syncResponse.itemValues.isNotEmpty()) { + Logger.i("Sync", "⬇️ Received from server:") + if (syncResponse.lists.isNotEmpty()) { + Logger.i("Sync", " 📋 ${syncResponse.lists.size} list(s)") + syncResponse.lists.forEach { list -> + if (!list.isDeleted) { + Logger.i("Sync", " ${list.name}") + } + } + } + if (syncResponse.fields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${syncResponse.fields.size} field(s)") + if (syncResponse.items.isNotEmpty()) Logger.i("Sync", " 📝 ${syncResponse.items.size} item(s)") + if (syncResponse.itemValues.isNotEmpty()) Logger.i("Sync", " 💾 ${syncResponse.itemValues.size} value(s)") } // Apply server changes to local database - Logger.d("Sync", "Inserting ${syncResponse.lists.size} lists into local database") database.listDao().insertLists(syncResponse.lists) - - Logger.d("Sync", "Inserting ${syncResponse.fields.size} fields into local database") database.fieldDao().insertFields(syncResponse.fields) - - Logger.d("Sync", "Inserting ${syncResponse.items.size} items into local database") database.itemDao().insertItems(syncResponse.items) - - Logger.d("Sync", "Inserting ${syncResponse.itemValues.size} item values into local database") database.itemValueDao().insertValues(syncResponse.itemValues) - // Verify what was actually saved - val savedLists = database.listDao().getListsUpdatedSince(0) - Logger.d("Sync", "After insert, total lists in DB: ${savedLists.size}") - savedLists.forEach { list -> - Logger.d("Sync", " DB list: ${list.id} - ${list.name} (deleted: ${list.isDeleted})") - } - // Update last sync timestamp setLastSyncTimestamp(syncResponse.serverTimestamp) - Logger.i("Sync", "Sync completed successfully. Timestamp updated to: ${syncResponse.serverTimestamp}") + if (isInitialSync) { + Logger.i("Sync", "✅ Initial sync completed") + } + return@withContext Result.success(Unit) } else { - Logger.e("Sync", "Sync failed with code: ${response.code()}, message: ${response.message()}") + Logger.e("Sync", "❌ Sync failed: HTTP ${response.code()}") return@withContext Result.failure(Exception("Sync failed: ${response.code()}")) } } catch (e: Exception) { - Logger.e("Sync", "Sync error", e) + Logger.e("Sync", "❌ Sync error: ${e.message}") return@withContext Result.failure(e) } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt index 131d6a3..765a514 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt @@ -71,35 +71,59 @@ fun ListsScreen( } } ) { padding -> - if (lists.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.no_lists), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(padding), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(lists, key = { it.id }) { list -> - ListItem( - list = list, - onListClick = { onNavigateToList(list.id) }, - onEditClick = { listToEdit = list }, - onDeleteClick = { listToDelete = list } + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + if (isLoading && lists.isEmpty()) { + // Show loading indicator during initial sync + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator() + Text( + text = "Syncing with server...", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } + } else if (lists.isEmpty()) { + // Show empty state + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.no_lists), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Tap + to create your first list", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + // Show lists + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(lists, key = { it.id }) { list -> + ListItem( + list = list, + onListClick = { onNavigateToList(list.id) }, + onEditClick = { listToEdit = list }, + onDeleteClick = { listToDelete = list } + ) + } + } } } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt index 90a77ab..f749ee1 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt @@ -27,9 +27,11 @@ class ListsViewModel( init { loadLists() - // Do initial sync immediately, then start periodic sync + // Perform initial sync immediately on startup, then start periodic sync viewModelScope.launch { - performSync() // Initial sync on startup + _isLoading.value = true + performSync() + _isLoading.value = false startPeriodicSync() } } @@ -50,12 +52,15 @@ class ListsViewModel( } private suspend fun performSync() { - syncRepository.performSync() + val result = syncRepository.performSync() + result.onFailure { error -> + Logger.e("Lists", "❌ Sync failed: ${error.message}") + } } fun createList(name: String) { viewModelScope.launch { - Logger.i("ListsViewModel", "Creating list: $name") + Logger.i("Lists", "➕ Creating list: \"$name\"") val timestamp = System.currentTimeMillis() val newList = CollabList( id = UUID.randomUUID().toString(), @@ -71,9 +76,9 @@ class ListsViewModel( fun renameList(listId: String, newName: String) { viewModelScope.launch { - Logger.i("ListsViewModel", "Renaming list: $listId to $newName") val list = database.listDao().getListById(listId) if (list != null && newName.isNotBlank()) { + Logger.i("Lists", "✏️ Renaming: \"${list.name}\" → \"$newName\"") database.listDao().updateList( list.copy( name = newName.trim(), @@ -88,16 +93,19 @@ class ListsViewModel( fun deleteList(listId: String) { viewModelScope.launch { - Logger.i("ListsViewModel", "Deleting list: $listId") - database.listDao().softDeleteList(listId, System.currentTimeMillis()) - // Sync immediately after deleting - performSync() + val list = database.listDao().getListById(listId) + if (list != null) { + Logger.i("Lists", "🗑️ Deleting list: \"${list.name}\"") + database.listDao().softDeleteList(listId, System.currentTimeMillis()) + // Sync immediately after deleting + performSync() + } } } fun manualSync() { viewModelScope.launch { - Logger.i("ListsViewModel", "Manual sync triggered") + Logger.i("Lists", "🔄 Manual sync requested") _isLoading.value = true performSync() _isLoading.value = false diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt index 9686598..662c073 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt @@ -3,22 +3,47 @@ package com.collabtable.app.ui.screens import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FilterList import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.collabtable.app.utils.LogEntry import com.collabtable.app.utils.LogLevel import com.collabtable.app.utils.Logger + +enum class TimeRange(val label: String, val milliseconds: Long) { + LAST_10_SECONDS("Last 10 seconds", 10_000), + LAST_30_SECONDS("Last 30 seconds", 30_000), + LAST_MINUTE("Last minute", 60_000), + LAST_5_MINUTES("Last 5 minutes", 300_000), + LAST_15_MINUTES("Last 15 minutes", 900_000), + LAST_HOUR("Last hour", 3_600_000), + LAST_24_HOURS("Last 24 hours", 86_400_000), + ALL_TIME("All time", Long.MAX_VALUE) +} + +data class LogFilters( + val severities: Set = LogLevel.values().toSet(), + val timeRange: TimeRange = TimeRange.ALL_TIME, + val tags: Set = emptySet(), + val searchText: String = "" +) + @OptIn(ExperimentalMaterial3Api::class) @Composable fun LogsScreen( @@ -26,24 +51,59 @@ fun LogsScreen( ) { val logs by Logger.logs.collectAsState() val listState = rememberLazyListState() + var showFilterSheet by remember { mutableStateOf(false) } + var filters by remember { mutableStateOf(LogFilters()) } + + // Extract all unique tags from logs + val allTags = remember(logs) { + logs.map { it.tag }.distinct().sorted() + } + + // Apply filters + val filteredLogs = remember(logs, filters) { + val now = System.currentTimeMillis() + val cutoffTime = now - filters.timeRange.milliseconds + + logs.filter { log -> + // Severity filter + log.level in filters.severities && + // Time range filter + log.timestamp >= cutoffTime && + // Tag filter (empty means show all) + (filters.tags.isEmpty() || log.tag in filters.tags) && + // Search text filter + (filters.searchText.isEmpty() || + log.message.contains(filters.searchText, ignoreCase = true) || + log.tag.contains(filters.searchText, ignoreCase = true)) + } + } // Auto-scroll to bottom when new logs arrive - LaunchedEffect(logs.size) { - if (logs.isNotEmpty()) { - listState.animateScrollToItem(logs.size - 1) + LaunchedEffect(filteredLogs.size) { + if (filteredLogs.isNotEmpty()) { + listState.animateScrollToItem(filteredLogs.size - 1) } } Scaffold( topBar = { TopAppBar( - title = { Text("Logs") }, + title = { Text("Logs (${filteredLogs.size}/${logs.size})") }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back") } }, actions = { + IconButton(onClick = { showFilterSheet = true }) { + Badge( + containerColor = if (hasActiveFilters(filters)) + MaterialTheme.colorScheme.error + else Color.Transparent + ) { + Icon(Icons.Default.FilterList, contentDescription = "Filter logs") + } + } IconButton(onClick = { Logger.clear() }) { Icon(Icons.Default.Delete, contentDescription = "Clear logs") } @@ -55,32 +115,299 @@ fun LogsScreen( ) } ) { padding -> - if (logs.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding), - contentAlignment = androidx.compose.ui.Alignment.Center - ) { - Text( - text = "No logs yet", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .padding(padding) - .background(Color(0xFF1E1E1E)), - contentPadding = PaddingValues(8.dp) - ) { - items(logs) { log -> - LogItem(log) + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + // Active filters chips + if (hasActiveFilters(filters)) { + LazyRow( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Severity filters + if (filters.severities.size < LogLevel.values().size) { + items(filters.severities.toList()) { level -> + FilterChip( + selected = true, + onClick = { + filters = filters.copy( + severities = filters.severities - level + ) + }, + label = { Text(level.name, fontSize = 12.sp) } + ) + } + } + + // Time range filter + if (filters.timeRange != TimeRange.ALL_TIME) { + item { + FilterChip( + selected = true, + onClick = { + filters = filters.copy(timeRange = TimeRange.ALL_TIME) + }, + label = { Text(filters.timeRange.label, fontSize = 12.sp) } + ) + } + } + + // Tag filters + items(filters.tags.toList()) { tag -> + FilterChip( + selected = true, + onClick = { + filters = filters.copy(tags = filters.tags - tag) + }, + label = { Text(tag, fontSize = 12.sp) } + ) + } + + // Clear all button + item { + FilterChip( + selected = false, + onClick = { + filters = LogFilters() + }, + label = { Text("Clear All", fontSize = 12.sp) }, + leadingIcon = { + Icon( + Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + ) + } } } + + // Logs list + if (filteredLogs.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = if (logs.isEmpty()) "No logs yet" else "No logs match filters", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF1E1E1E)), + contentPadding = PaddingValues(8.dp) + ) { + items(filteredLogs) { log -> + LogItem(log) + } + } + } + } + } + + // Filter bottom sheet + if (showFilterSheet) { + FilterBottomSheet( + filters = filters, + allTags = allTags, + onFiltersChanged = { filters = it }, + onDismiss = { showFilterSheet = false } + ) + } +} + +private fun hasActiveFilters(filters: LogFilters): Boolean { + return filters.severities.size < LogLevel.values().size || + filters.timeRange != TimeRange.ALL_TIME || + filters.tags.isNotEmpty() || + filters.searchText.isNotEmpty() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterBottomSheet( + filters: LogFilters, + allTags: List, + onFiltersChanged: (LogFilters) -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp) + ) { + Text( + text = "Filter Logs", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + + // Severity filters + Text( + text = "Severity", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 8.dp) + ) + + LogLevel.values().forEach { level -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = level in filters.severities, + onCheckedChange = { checked -> + onFiltersChanged( + filters.copy( + severities = if (checked) { + filters.severities + level + } else { + filters.severities - level + } + ) + ) + } + ) + Text( + text = level.name, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + + Divider(modifier = Modifier.padding(vertical = 16.dp)) + + // Time range filter + Text( + text = "Time Range", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 8.dp) + ) + + TimeRange.values().forEach { range -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = filters.timeRange == range, + onClick = { + onFiltersChanged(filters.copy(timeRange = range)) + } + ) + Text( + text = range.label, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + + Divider(modifier = Modifier.padding(vertical = 16.dp)) + + // Tag filters + Text( + text = "Tags (${filters.tags.size} selected)", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 8.dp) + ) + + if (allTags.isEmpty()) { + Text( + text = "No tags available", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + onClick = { + onFiltersChanged(filters.copy(tags = allTags.toSet())) + } + ) { + Text("Select All") + } + TextButton( + onClick = { + onFiltersChanged(filters.copy(tags = emptySet())) + } + ) { + Text("Clear All") + } + } + + allTags.forEach { tag -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = tag in filters.tags, + onCheckedChange = { checked -> + onFiltersChanged( + filters.copy( + tags = if (checked) { + filters.tags + tag + } else { + filters.tags - tag + } + ) + ) + } + ) + Text( + text = tag, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + + Divider(modifier = Modifier.padding(vertical = 16.dp)) + + // Reset button + Button( + onClick = { + onFiltersChanged(LogFilters()) + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Reset All Filters") + } } } } diff --git a/CollabTableAndroid/build-output.txt b/CollabTableAndroid/build-output.txt index cea9e63..2b6f476 100644 --- a/CollabTableAndroid/build-output.txt +++ b/CollabTableAndroid/build-output.txt @@ -32,12 +32,8 @@ > Task :app:writeDebugAppMetadata UP-TO-DATE > Task :app:writeDebugSigningConfigVersions UP-TO-DATE > Task :app:kspDebugKotlin - > Task :app:compileDebugKotlin -w: file:///C:/Users/gabri/VSCode%20Projects/CollabTable/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt:1336:72 Elvis operator (?:) always returns the left operand of non-nullable type String -w: file:///C:/Users/gabri/VSCode%20Projects/CollabTable/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt:2126:28 The expression is unused - -> Task :app:compileDebugJavaWithJavac +> Task :app:compileDebugJavaWithJavac UP-TO-DATE > Task :app:dexBuilderDebug > Task :app:mergeDebugGlobalSynthetics UP-TO-DATE > Task :app:processDebugJavaRes UP-TO-DATE @@ -53,5 +49,5 @@ You can use '--warning-mode all' to show the individual deprecation warnings and For more on this, please refer to https://docs.gradle.org/8.10/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. -BUILD SUCCESSFUL in 38s -36 actionable tasks: 7 executed, 29 up-to-date +BUILD SUCCESSFUL in 10s +36 actionable tasks: 6 executed, 30 up-to-date diff --git a/CollabTableServer/src/routes/syncRoutes.ts b/CollabTableServer/src/routes/syncRoutes.ts index 9e40528..ef84489 100644 --- a/CollabTableServer/src/routes/syncRoutes.ts +++ b/CollabTableServer/src/routes/syncRoutes.ts @@ -25,13 +25,20 @@ let syncStats = { lastReset: Date.now() }; -// Print stats every 30 seconds +// Print stats every 60 seconds (reduced spam) setInterval(() => { if (syncStats.totalSyncs > 0) { const elapsed = Math.round((Date.now() - syncStats.lastReset) / 1000); - console.log(`\n[STATS] Last ${elapsed}s: ${syncStats.totalSyncs} syncs`); - console.log(` Received: ${syncStats.listsReceived} lists, ${syncStats.fieldsReceived} fields, ${syncStats.itemsReceived} items, ${syncStats.valuesReceived} values`); - console.log(` Sent: ${syncStats.listsSent} lists, ${syncStats.fieldsSent} fields, ${syncStats.itemsSent} items, ${syncStats.valuesSent} values\n`); + console.log(`\n[STATS] Sync Summary (last ${elapsed}s):`); + console.log(` ${syncStats.totalSyncs} sync requests processed`); + + if (syncStats.listsReceived > 0 || syncStats.fieldsReceived > 0 || syncStats.itemsReceived > 0 || syncStats.valuesReceived > 0) { + console.log(` [IN] Received: ${syncStats.listsReceived} lists, ${syncStats.fieldsReceived} fields, ${syncStats.itemsReceived} items, ${syncStats.valuesReceived} values`); + } + + if (syncStats.listsSent > 0 || syncStats.fieldsSent > 0 || syncStats.itemsSent > 0 || syncStats.valuesSent > 0) { + console.log(` [OUT] Sent: ${syncStats.listsSent} lists, ${syncStats.fieldsSent} fields, ${syncStats.itemsSent} items, ${syncStats.valuesSent} values`); + } // Reset stats syncStats = { @@ -47,7 +54,7 @@ setInterval(() => { lastReset: Date.now() }; } -}, 30000); +}, 60000); // Changed from 30s to 60s // Helper function to get prepared statements (lazy initialization) function getUpsertStatements() { @@ -104,16 +111,36 @@ router.post('/sync', async (req: Request, res: Response) => { syncStats.itemsReceived += incomingItems; syncStats.valuesReceived += incomingValues; - // Log only when there's actual data being synced + // Only log significant sync events (not empty syncs) const hasIncomingData = incomingLists > 0 || incomingFields > 0 || incomingItems > 0 || incomingValues > 0; + const isInitialSync = lastSyncTimestamp === 0; + if (hasIncomingData) { - console.log(`[SYNC] Received: ${incomingLists} lists, ${incomingFields} fields, ${incomingItems} items, ${incomingValues} values`); - // Log list details - if (lists && lists.length > 0) { + const timestamp = new Date().toLocaleTimeString(); + console.log(`\n[SYNC] [${timestamp}] Receiving data from client`); + + if (incomingLists > 0) { + console.log(` [LISTS] ${incomingLists} list(s)`); lists.forEach(list => { - console.log(` List: ${list.id} - "${list.name}" (updated: ${list.updatedAt}, deleted: ${list.isDeleted})`); + const action = list.isDeleted ? 'Deleted' : (lastSyncTimestamp === 0 ? 'Created' : 'Updated'); + console.log(` ${action}: "${list.name}"`); }); } + + if (incomingFields > 0) { + console.log(` [FIELDS] ${incomingFields} field(s)`); + } + + if (incomingItems > 0) { + console.log(` [ITEMS] ${incomingItems} item(s)`); + } + + if (incomingValues > 0) { + console.log(` [VALUES] ${incomingValues} value(s)`); + } + } else if (isInitialSync) { + const timestamp = new Date().toLocaleTimeString(); + console.log(`\n[SYNC] [${timestamp}] Initial sync from new client`); } // Save incoming data from client using a transaction @@ -130,7 +157,6 @@ router.post('/sync', async (req: Request, res: Response) => { updatedAt: list.updatedAt, isDeleted: list.isDeleted ? 1 : 0 }); - console.log(` Saved list ${list.id}`); } } @@ -210,10 +236,12 @@ router.post('/sync', async (req: Request, res: Response) => { syncStats.itemsSent += (serverItems as any[]).length; syncStats.valuesSent += (serverItemValues as any[]).length; - // Log only when sending data back + // Only log when sending significant data back const hasOutgoingData = (serverLists as any[]).length > 0 || (serverFields as any[]).length > 0 || (serverItems as any[]).length > 0 || (serverItemValues as any[]).length > 0; - if (hasOutgoingData) { - console.log(`[SYNC] Sending: ${(serverLists as any[]).length} lists, ${(serverFields as any[]).length} fields, ${(serverItems as any[]).length} items, ${(serverItemValues as any[]).length} values`); + if (hasOutgoingData && !isInitialSync) { + console.log(` [OUT] Sending back: ${(serverLists as any[]).length} lists, ${(serverFields as any[]).length} fields, ${(serverItems as any[]).length} items, ${(serverItemValues as any[]).length} values`); + } else if (isInitialSync && hasOutgoingData) { + console.log(` [OUT] Sending initial data: ${(serverLists as any[]).length} lists, ${(serverFields as any[]).length} fields, ${(serverItems as any[]).length} items, ${(serverItemValues as any[]).length} values`); } res.json({ @@ -224,7 +252,7 @@ router.post('/sync', async (req: Request, res: Response) => { serverTimestamp }); } catch (error) { - console.error('[SYNC] Error:', error); + console.error('[ERROR] Sync error:', error); res.status(500).json({ error: 'Sync failed' }); } }); diff --git a/CollabTableServer/src/routes/webRoutes.ts b/CollabTableServer/src/routes/webRoutes.ts index 1f02c66..619ea2c 100644 --- a/CollabTableServer/src/routes/webRoutes.ts +++ b/CollabTableServer/src/routes/webRoutes.ts @@ -189,7 +189,10 @@ router.get('/', (req: Request, res: Response) => {