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/" val ensuredApi = if (rawUrl.contains("/api")) rawUrl else rawUrl.trimEnd('/') + "/api/"
ensureTrailingSlash(ensuredApi) ensureTrailingSlash(ensuredApi)
} }
} catch (e: Exception) { } catch (error: Exception) {
Logger.w("HTTP", "Failed to normalize API URL '$rawUrl': ${error.message}")
ensureTrailingSlash(rawUrl) ensureTrailingSlash(rawUrl)
} }
} }
@@ -73,11 +73,12 @@ data class Field(
) { ) {
fun getType(): FieldType { fun getType(): FieldType {
val raw = fieldType.ifBlank { "TEXT" }.uppercase() val raw = fieldType.ifBlank { "TEXT" }.uppercase()
return try { val exact = runCatching { FieldType.valueOf(raw) }.getOrNull()
FieldType.valueOf(raw) if (exact != null) {
} catch (e: Exception) { return exact
}
// Handle legacy/synonym field types // Handle legacy/synonym field types
when (raw) { return when (raw) {
"DROPDOWN" -> FieldType.SELECT "DROPDOWN" -> FieldType.SELECT
"STRING" -> FieldType.TEXT "STRING" -> FieldType.TEXT
"PRICE" -> FieldType.CURRENCY "PRICE" -> FieldType.CURRENCY
@@ -86,7 +87,6 @@ data class Field(
else -> FieldType.TEXT else -> FieldType.TEXT
} }
} }
}
fun getSelectOptions(): List<String> = fun getSelectOptions(): List<String> =
if ((getType() == FieldType.SELECT || getType() == FieldType.MULTI_SELECT) && fieldOptions.isNotBlank()) { if ((getType() == FieldType.SELECT || getType() == FieldType.MULTI_SELECT) && fieldOptions.isNotBlank()) {
@@ -194,7 +194,7 @@ class PreferencesManager(
fun getColumnWidths(listId: String): Map<String, Float> { fun getColumnWidths(listId: String): Map<String, Float> {
val key = COLUMN_WIDTHS_PREFIX + listId val key = COLUMN_WIDTHS_PREFIX + listId
val raw = prefs.getString(key, null) ?: return emptyMap() val raw = prefs.getString(key, null) ?: return emptyMap()
return try { return runCatching {
val json = JSONObject(raw) val json = JSONObject(raw)
val map = mutableMapOf<String, Float>() val map = mutableMapOf<String, Float>()
val it = json.keys() val it = json.keys()
@@ -206,9 +206,7 @@ class PreferencesManager(
} }
} }
map map
} catch (e: Exception) { }.getOrDefault(emptyMap())
emptyMap()
}
} }
fun setColumnWidths( fun setColumnWidths(
@@ -229,7 +227,7 @@ class PreferencesManager(
fun getColumnAlignments(listId: String): Map<String, String> { fun getColumnAlignments(listId: String): Map<String, String> {
val key = COLUMN_ALIGN_PREFIX + listId val key = COLUMN_ALIGN_PREFIX + listId
val raw = prefs.getString(key, null) ?: return emptyMap() val raw = prefs.getString(key, null) ?: return emptyMap()
return try { return runCatching {
val json = JSONObject(raw) val json = JSONObject(raw)
val map = mutableMapOf<String, String>() val map = mutableMapOf<String, String>()
val it = json.keys() val it = json.keys()
@@ -245,9 +243,7 @@ class PreferencesManager(
map[fieldId] = normalized map[fieldId] = normalized
} }
map map
} catch (e: Exception) { }.getOrDefault(emptyMap())
emptyMap()
}
} }
fun setColumnAlignments( fun setColumnAlignments(
@@ -312,14 +312,14 @@ class SyncRepository(
private fun isNetworkAvailable(): Boolean { private fun isNetworkAvailable(): Boolean {
val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetwork ?: return false val network = cm.activeNetwork
val caps = cm.getNetworkCapabilities(network) ?: return false val caps = network?.let { cm.getNetworkCapabilities(it) }
val hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) val hasInternet = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
val isValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) val isValidated = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
val hasTransport = val hasTransport =
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) caps?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true
return hasInternet && isValidated && hasTransport return hasInternet && isValidated && hasTransport
} }
} }
@@ -53,7 +53,7 @@ private fun isEmulator(): Boolean {
private fun toHealthUrl(rawApiUrl: String): String { private fun toHealthUrl(rawApiUrl: String): String {
// Ensure we hit the unauthenticated /health endpoint and remap localhost on emulator // Ensure we hit the unauthenticated /health endpoint and remap localhost on emulator
return try { return runCatching {
val uri = Uri.parse(rawApiUrl) val uri = Uri.parse(rawApiUrl)
val host = uri.host?.lowercase() val host = uri.host?.lowercase()
val needsRemap = isEmulator() && (host == "localhost" || host == "127.0.0.1" || host == "host.docker.internal") 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 path = (uri.encodedPath ?: "/").trimEnd('/')
val healthPath = if (path.endsWith("/api") || path.endsWith("/api/")) "/health" else "/health" val healthPath = if (path.endsWith("/api") || path.endsWith("/api/")) "/health" else "/health"
base + healthPath base + healthPath
} catch (e: Exception) { }.getOrElse {
rawApiUrl.replace("/api/", "/health").replace("/api", "/health") rawApiUrl.replace("/api/", "/health").replace("/api", "/health")
} }
} }
@@ -101,7 +101,7 @@ fun ConnectionStatusAction(
ok = false ok = false
} }
resp.close() resp.close()
} catch (e: Exception) { } catch (ignored: Exception) {
ok = false ok = false
} }
delay(intervalMs) 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) @file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.collabtable.app.ui.screens package com.collabtable.app.ui.screens
@@ -1,8 +1,6 @@
package com.collabtable.app.ui.screens package com.collabtable.app.ui.screens
import android.content.Context import android.content.Context
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.room.withTransaction import androidx.room.withTransaction
@@ -123,11 +121,6 @@ class ListDetailViewModel(
syncRepository.performSync() 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. // 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 */ } 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.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable import org.burnoutcrew.reorderable.reorderable
@Suppress("UnusedParameter")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ListsScreen( fun ListsScreen(
@@ -276,7 +277,7 @@ fun ListsScreen(
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(0.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) { _ -> ReorderableItem(reorderState, key = list.id) { _ ->
Box(modifier = Modifier.animateItemPlacement()) { Box(modifier = Modifier.animateItemPlacement()) {
ListItem( ListItem(
@@ -233,6 +233,7 @@ class ListsViewModel(
* stored in the app's external files directory. Returns a Result containing * stored in the app's external files directory. Returns a Result containing
* the absolute file path on success. * the absolute file path on success.
*/ */
@Suppress("ReturnCount")
suspend fun exportListToCsv( suspend fun exportListToCsv(
listId: String, listId: String,
listName: String, listName: String,
@@ -1,3 +1,5 @@
@file:Suppress("SwallowedException")
package com.collabtable.app.ui.screens package com.collabtable.app.ui.screens
import android.net.Uri import android.net.Uri
@@ -64,6 +64,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Suppress("UnusedParameter")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -84,8 +84,13 @@ object Logger {
): Boolean { ): Boolean {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val last = _logs.value.lastOrNull() 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 // 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 return false
} }
val entry = LogEntry(timestamp = now, level = level, tag = tag, message = message) 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.model.CollabList
import com.collabtable.app.data.preferences.PreferencesManager import com.collabtable.app.data.preferences.PreferencesManager
import com.collabtable.app.notifications.NotificationHelper import com.collabtable.app.notifications.NotificationHelper
import com.collabtable.app.utils.Logger
class ListChangeNotificationWorker( class ListChangeNotificationWorker(
appContext: Context, appContext: Context,
@@ -28,7 +29,8 @@ class ListChangeNotificationWorker(
// Update checkpoint regardless to avoid duplicate spam // Update checkpoint regardless to avoid duplicate spam
prefs.setLastListNotifyCheckTimestamp(now) prefs.setLastListNotifyCheckTimestamp(now)
Result.success() Result.success()
} catch (e: Exception) { } catch (error: Exception) {
Logger.w("ListNotifyWorker", "Background notification polling failed: ${error.message}")
Result.retry() Result.retry()
} }
} }
+4 -4
View File
@@ -27,7 +27,7 @@
"@types/node": "^20.10.6", "@types/node": "^20.10.6",
"@types/pg": "^8.10.2", "@types/pg": "^8.10.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5.3.3" "typescript": "5.5.4"
} }
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -3607,9 +3607,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
+1 -1
View File
@@ -32,7 +32,7 @@
"@types/node": "^20.10.6", "@types/node": "^20.10.6",
"@types/better-sqlite3": "^7.6.11", "@types/better-sqlite3": "^7.6.11",
"@types/pg": "^8.10.2", "@types/pg": "^8.10.2",
"typescript": "^5.3.3", "typescript": "5.5.4",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0"
} }
} }