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/"
|
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,18 +73,18 @@ 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
|
}
|
||||||
when (raw) {
|
// Handle legacy/synonym field types
|
||||||
"DROPDOWN" -> FieldType.SELECT
|
return when (raw) {
|
||||||
"STRING" -> FieldType.TEXT
|
"DROPDOWN" -> FieldType.SELECT
|
||||||
"PRICE" -> FieldType.CURRENCY
|
"STRING" -> FieldType.TEXT
|
||||||
"AMOUNT" -> FieldType.NUMBER
|
"PRICE" -> FieldType.CURRENCY
|
||||||
"SIZE" -> FieldType.TEXT
|
"AMOUNT" -> FieldType.NUMBER
|
||||||
else -> FieldType.TEXT
|
"SIZE" -> FieldType.TEXT
|
||||||
}
|
else -> FieldType.TEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -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(
|
||||||
|
|||||||
+7
-7
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -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)
|
||||||
|
|||||||
+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)
|
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
|
|
||||||
package com.collabtable.app.ui.screens
|
package com.collabtable.app.ui.screens
|
||||||
|
|||||||
-7
@@ -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,
|
||||||
|
|||||||
+2
@@ -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)
|
||||||
|
|||||||
+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.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user