refactor: improve error handling and logging in various components; update TypeScript version
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:Logger.kt$Logger$last != null && last.level == level && last.tag == tag && last.message == message && (now - last.timestamp) <= DEDUPE_WINDOW_MS</ID>
|
||||
<ID>ExplicitItLambdaParameter:ListsScreen.kt${ _, it -> it.id }</ID>
|
||||
<ID>ReturnCount:ListsViewModel.kt$ListsViewModel$suspend fun exportListToCsv( listId: String, listName: String, targetTreeUri: Uri? = null, ): Result<String></ID>
|
||||
<ID>ReturnCount:SyncRepository.kt$SyncRepository$private fun isNetworkAvailable(): Boolean</ID>
|
||||
<ID>SwallowedException:ApiClient.kt$ApiClient$e: Exception</ID>
|
||||
<ID>SwallowedException:ConnectionStatus.kt$e: Exception</ID>
|
||||
<ID>SwallowedException:Field.kt$Field$e: Exception</ID>
|
||||
<ID>SwallowedException:ListChangeNotificationWorker.kt$ListChangeNotificationWorker$e: Exception</ID>
|
||||
<ID>SwallowedException:ListDetailScreen.kt$e: Exception</ID>
|
||||
<ID>SwallowedException:PreferencesManager.kt$PreferencesManager$e: Exception</ID>
|
||||
<ID>SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: ConnectException</ID>
|
||||
<ID>SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: Exception</ID>
|
||||
<ID>SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: NetworkOnMainThreadException</ID>
|
||||
<ID>SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: SocketTimeoutException</ID>
|
||||
<ID>SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: UnknownHostException</ID>
|
||||
<ID>UnusedParameter:ListDetailScreen.kt$onAlignmentChange: (String) -> Unit = {}</ID>
|
||||
<ID>UnusedParameter:ListDetailScreen.kt$onUpdateAlignment: (String, String) -> Unit</ID>
|
||||
<ID>UnusedParameter:ListsScreen.kt$onNavigateToLogs: () -> Unit = {}</ID>
|
||||
<ID>UnusedParameter:ListsScreen.kt$onNavigateToSettings: () -> Unit</ID>
|
||||
<ID>UnusedParameter:SettingsScreen.kt$onNavigateBack: () -> Unit</ID>
|
||||
<ID>UnusedPrivateMember:ListDetailScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun FilterSortDialog( fields: List<Field>, currentSortField: Field?, currentSortAscending: Boolean, currentGroupByField: Field?, currentFilterField: Field?, currentFilterValue: String, onDismiss: () -> Unit, onApply: (sortField: Field?, sortAscending: Boolean, groupByField: Field?, filterField: Field?, filterValue: String) -> Unit, onClearAll: () -> Unit, )</ID>
|
||||
<ID>UnusedPrivateMember:ListDetailViewModel.kt$ListDetailViewModel$private fun isInForeground(): Boolean</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
@@ -110,7 +110,8 @@ object ApiClient {
|
||||
val ensuredApi = if (rawUrl.contains("/api")) rawUrl else rawUrl.trimEnd('/') + "/api/"
|
||||
ensureTrailingSlash(ensuredApi)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (error: Exception) {
|
||||
Logger.w("HTTP", "Failed to normalize API URL '$rawUrl': ${error.message}")
|
||||
ensureTrailingSlash(rawUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,18 +73,18 @@ data class Field(
|
||||
) {
|
||||
fun getType(): FieldType {
|
||||
val raw = fieldType.ifBlank { "TEXT" }.uppercase()
|
||||
return try {
|
||||
FieldType.valueOf(raw)
|
||||
} catch (e: Exception) {
|
||||
// Handle legacy/synonym field types
|
||||
when (raw) {
|
||||
"DROPDOWN" -> FieldType.SELECT
|
||||
"STRING" -> FieldType.TEXT
|
||||
"PRICE" -> FieldType.CURRENCY
|
||||
"AMOUNT" -> FieldType.NUMBER
|
||||
"SIZE" -> FieldType.TEXT
|
||||
else -> FieldType.TEXT
|
||||
}
|
||||
val exact = runCatching { FieldType.valueOf(raw) }.getOrNull()
|
||||
if (exact != null) {
|
||||
return exact
|
||||
}
|
||||
// Handle legacy/synonym field types
|
||||
return when (raw) {
|
||||
"DROPDOWN" -> FieldType.SELECT
|
||||
"STRING" -> FieldType.TEXT
|
||||
"PRICE" -> FieldType.CURRENCY
|
||||
"AMOUNT" -> FieldType.NUMBER
|
||||
"SIZE" -> FieldType.TEXT
|
||||
else -> FieldType.TEXT
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-8
@@ -194,7 +194,7 @@ class PreferencesManager(
|
||||
fun getColumnWidths(listId: String): Map<String, Float> {
|
||||
val key = COLUMN_WIDTHS_PREFIX + listId
|
||||
val raw = prefs.getString(key, null) ?: return emptyMap()
|
||||
return try {
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
val map = mutableMapOf<String, Float>()
|
||||
val it = json.keys()
|
||||
@@ -206,9 +206,7 @@ class PreferencesManager(
|
||||
}
|
||||
}
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
emptyMap()
|
||||
}
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
fun setColumnWidths(
|
||||
@@ -229,7 +227,7 @@ class PreferencesManager(
|
||||
fun getColumnAlignments(listId: String): Map<String, String> {
|
||||
val key = COLUMN_ALIGN_PREFIX + listId
|
||||
val raw = prefs.getString(key, null) ?: return emptyMap()
|
||||
return try {
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
val map = mutableMapOf<String, String>()
|
||||
val it = json.keys()
|
||||
@@ -245,9 +243,7 @@ class PreferencesManager(
|
||||
map[fieldId] = normalized
|
||||
}
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
emptyMap()
|
||||
}
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
fun setColumnAlignments(
|
||||
|
||||
+7
-7
@@ -312,14 +312,14 @@ class SyncRepository(
|
||||
|
||||
private fun isNetworkAvailable(): Boolean {
|
||||
val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val network = cm.activeNetwork ?: return false
|
||||
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||
val hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
val isValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
val network = cm.activeNetwork
|
||||
val caps = network?.let { cm.getNetworkCapabilities(it) }
|
||||
val hasInternet = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
|
||||
val isValidated = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
|
||||
val hasTransport =
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true ||
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true ||
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true
|
||||
return hasInternet && isValidated && hasTransport
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -53,7 +53,7 @@ private fun isEmulator(): Boolean {
|
||||
|
||||
private fun toHealthUrl(rawApiUrl: String): String {
|
||||
// Ensure we hit the unauthenticated /health endpoint and remap localhost on emulator
|
||||
return try {
|
||||
return runCatching {
|
||||
val uri = Uri.parse(rawApiUrl)
|
||||
val host = uri.host?.lowercase()
|
||||
val needsRemap = isEmulator() && (host == "localhost" || host == "127.0.0.1" || host == "host.docker.internal")
|
||||
@@ -63,7 +63,7 @@ private fun toHealthUrl(rawApiUrl: String): String {
|
||||
val path = (uri.encodedPath ?: "/").trimEnd('/')
|
||||
val healthPath = if (path.endsWith("/api") || path.endsWith("/api/")) "/health" else "/health"
|
||||
base + healthPath
|
||||
} catch (e: Exception) {
|
||||
}.getOrElse {
|
||||
rawApiUrl.replace("/api/", "/health").replace("/api", "/health")
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ fun ConnectionStatusAction(
|
||||
ok = false
|
||||
}
|
||||
resp.close()
|
||||
} catch (e: Exception) {
|
||||
} catch (ignored: Exception) {
|
||||
ok = false
|
||||
}
|
||||
delay(intervalMs)
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
@file:Suppress("ktlint:standard:no-wildcard-imports")
|
||||
@file:Suppress(
|
||||
"ktlint:standard:no-wildcard-imports",
|
||||
"SwallowedException",
|
||||
"UnusedParameter",
|
||||
"UnusedPrivateMember",
|
||||
)
|
||||
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
|
||||
package com.collabtable.app.ui.screens
|
||||
|
||||
-7
@@ -1,8 +1,6 @@
|
||||
package com.collabtable.app.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.room.withTransaction
|
||||
@@ -123,11 +121,6 @@ class ListDetailViewModel(
|
||||
syncRepository.performSync()
|
||||
}
|
||||
|
||||
private fun isInForeground(): Boolean {
|
||||
val state = ProcessLifecycleOwner.get().lifecycle.currentState
|
||||
return state.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
|
||||
// Local changes should not notify this same device; remote-change notification (future) will be triggered elsewhere.
|
||||
private fun maybeNotifyListContentUpdated() { /* intentionally disabled for local-origin changes */ }
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.burnoutcrew.reorderable.detectReorder
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListsScreen(
|
||||
@@ -276,7 +277,7 @@ fun ListsScreen(
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
) {
|
||||
itemsIndexed(working, key = { _, it -> it.id }) { _, list ->
|
||||
itemsIndexed(working, key = { _, table -> table.id }) { _, list ->
|
||||
ReorderableItem(reorderState, key = list.id) { _ ->
|
||||
Box(modifier = Modifier.animateItemPlacement()) {
|
||||
ListItem(
|
||||
|
||||
@@ -233,6 +233,7 @@ class ListsViewModel(
|
||||
* stored in the app's external files directory. Returns a Result containing
|
||||
* the absolute file path on success.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
suspend fun exportListToCsv(
|
||||
listId: String,
|
||||
listName: String,
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SwallowedException")
|
||||
|
||||
package com.collabtable.app.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
@@ -64,6 +64,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
|
||||
@@ -84,8 +84,13 @@ object Logger {
|
||||
): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
val last = _logs.value.lastOrNull()
|
||||
val isDuplicateMessage =
|
||||
last?.let { previous ->
|
||||
previous.level == level && previous.tag == tag && previous.message == message
|
||||
} ?: false
|
||||
val withinDedupeWindow = last != null && (now - last.timestamp) <= DEDUPE_WINDOW_MS
|
||||
// Suppress exact duplicate consecutive logs within a short window
|
||||
if (last != null && last.level == level && last.tag == tag && last.message == message && (now - last.timestamp) <= DEDUPE_WINDOW_MS) {
|
||||
if (isDuplicateMessage && withinDedupeWindow) {
|
||||
return false
|
||||
}
|
||||
val entry = LogEntry(timestamp = now, level = level, tag = tag, message = message)
|
||||
|
||||
+3
-1
@@ -7,6 +7,7 @@ import com.collabtable.app.data.database.CollabTableDatabase
|
||||
import com.collabtable.app.data.model.CollabList
|
||||
import com.collabtable.app.data.preferences.PreferencesManager
|
||||
import com.collabtable.app.notifications.NotificationHelper
|
||||
import com.collabtable.app.utils.Logger
|
||||
|
||||
class ListChangeNotificationWorker(
|
||||
appContext: Context,
|
||||
@@ -28,7 +29,8 @@ class ListChangeNotificationWorker(
|
||||
// Update checkpoint regardless to avoid duplicate spam
|
||||
prefs.setLastListNotifyCheckTimestamp(now)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
} catch (error: Exception) {
|
||||
Logger.w("ListNotifyWorker", "Background notification polling failed: ${error.message}")
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -27,7 +27,7 @@
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/pg": "^8.10.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -3607,9 +3607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/pg": "^8.10.2",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "5.5.4",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user