refactor: improve error handling and logging in various components; update TypeScript version

This commit is contained in:
2026-06-03 17:38:23 +02:00
parent ebe5fc13c4
commit e57b14ef17
16 changed files with 54 additions and 75 deletions
@@ -1,28 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:Logger.kt$Logger$last != null &amp;&amp; last.level == level &amp;&amp; last.tag == tag &amp;&amp; last.message == message &amp;&amp; (now - last.timestamp) &lt;= DEDUPE_WINDOW_MS</ID>
<ID>ExplicitItLambdaParameter:ListsScreen.kt${ _, it -&gt; it.id }</ID>
<ID>ReturnCount:ListsViewModel.kt$ListsViewModel$suspend fun exportListToCsv( listId: String, listName: String, targetTreeUri: Uri? = null, ): Result&lt;String&gt;</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) -&gt; Unit = {}</ID>
<ID>UnusedParameter:ListDetailScreen.kt$onUpdateAlignment: (String, String) -&gt; Unit</ID>
<ID>UnusedParameter:ListsScreen.kt$onNavigateToLogs: () -&gt; Unit = {}</ID>
<ID>UnusedParameter:ListsScreen.kt$onNavigateToSettings: () -&gt; Unit</ID>
<ID>UnusedParameter:SettingsScreen.kt$onNavigateBack: () -&gt; Unit</ID>
<ID>UnusedPrivateMember:ListDetailScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun FilterSortDialog( fields: List&lt;Field&gt;, currentSortField: Field?, currentSortAscending: Boolean, currentGroupByField: Field?, currentFilterField: Field?, currentFilterValue: String, onDismiss: () -&gt; Unit, onApply: (sortField: Field?, sortAscending: Boolean, groupByField: Field?, filterField: Field?, filterValue: String) -&gt; Unit, onClearAll: () -&gt; 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,11 +73,12 @@ data class Field(
) {
fun getType(): FieldType {
val raw = fieldType.ifBlank { "TEXT" }.uppercase()
return try {
FieldType.valueOf(raw)
} catch (e: Exception) {
val exact = runCatching { FieldType.valueOf(raw) }.getOrNull()
if (exact != null) {
return exact
}
// Handle legacy/synonym field types
when (raw) {
return when (raw) {
"DROPDOWN" -> FieldType.SELECT
"STRING" -> FieldType.TEXT
"PRICE" -> FieldType.CURRENCY
@@ -86,7 +87,6 @@ data class Field(
else -> FieldType.TEXT
}
}
}
fun getSelectOptions(): List<String> =
if ((getType() == FieldType.SELECT || getType() == FieldType.MULTI_SELECT) && fieldOptions.isNotBlank()) {
@@ -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(
@@ -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
}
}
@@ -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)
@@ -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
@@ -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,
@@ -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)
@@ -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()
}
}
+4 -4
View File
@@ -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",
+1 -1
View File
@@ -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"
}
}