From ecb27a9279c640ddf3b7841c07055fae20d10c31 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:21:18 +0500 Subject: [PATCH 1/8] feat: Add JSON export endpoint for network logs with device ID and timestamp filtering - Create NetworkLogsExportModel data classes with serialization support - Implement ExportNetworkLogsAsJsonUseCase for filtering network logs - Add GET /api/export/network-logs endpoint with query parameter filtering - Support filtering by deviceId, startTimestamp, and endTimestamp - Return JSON response with metadata about exported data --- .../remote/models/NetworkLogsExportModel.kt | 41 ++++++++ .../remote/server/NetworkExportEndpoint.kt | 94 +++++++++++++++++++ .../usecase/ExportNetworkLogsAsJsonUseCase.kt | 37 ++++++++ 3 files changed, 172 insertions(+) create mode 100644 FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt create mode 100644 FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt new file mode 100644 index 000000000..682f05170 --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt @@ -0,0 +1,41 @@ +package com.flocon.data.remote.models + +import kotlinx.serialization.Serializable + +@Serializable +data class NetworkLogsExportResponse( + val data: List, + val metadata: ExportMetadata, +) + +@Serializable +data class ExportMetadata( + val exportedAt: Long, + val totalItems: Int, + val filteredBy: FilterCriteria?, +) + +@Serializable +data class FilterCriteria( + val deviceId: String? = null, + val startTimestamp: Long? = null, + val endTimestamp: Long? = null, +) + +@Serializable +data class NetworkCallExport( + val callId: String, + val method: String, + val url: String, + val startTime: Long, + val startTimeFormatted: String, + val statusCode: Int? = null, + val durationMs: Double? = null, + val requestHeaders: Map, + val responseHeaders: Map? = null, + val requestBody: String? = null, + val responseBody: String? = null, + val contentType: String? = null, + val deviceId: String, + val appInstance: Long, +) \ No newline at end of file diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt new file mode 100644 index 000000000..5da655673 --- /dev/null +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt @@ -0,0 +1,94 @@ +package com.flocon.data.remote.server + +import co.touchlab.kermit.Logger +import com.flocon.data.remote.models.ExportMetadata +import com.flocon.data.remote.models.FilterCriteria +import com.flocon.data.remote.models.NetworkCallExport +import com.flocon.data.remote.models.NetworkLogsExportResponse +import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get + +fun Route.networkExportRoutes( + getNetworkCalls: suspend () -> List, +) { + get("/api/export/network-logs") { + try { + val deviceId = call.request.queryParameters["deviceId"] + val startTimestamp = call.request.queryParameters["startTimestamp"]?.toLongOrNull() + val endTimestamp = call.request.queryParameters["endTimestamp"]?.toLongOrNull() + + val allCalls = getNetworkCalls() + + val filtered = allCalls.filter { call -> + val matchesDevice = deviceId == null || call.appInstance.deviceId == deviceId + val matchesStartTime = startTimestamp == null || call.request.startTime >= startTimestamp + val matchesEndTime = endTimestamp == null || call.request.startTime <= endTimestamp + + matchesDevice && matchesStartTime && matchesEndTime + } + + val exportedCalls = filtered.map { call -> + NetworkCallExport( + callId = call.callId, + method = call.request.method, + url = call.request.url, + startTime = call.request.startTime, + startTimeFormatted = call.request.startTimeFormatted, + statusCode = when (val response = call.response) { + is FloconNetworkCallDomainModel.Response.Success -> + when (val info = response.specificInfos) { + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> info.httpCode + else -> null + } + else -> null + }, + durationMs = call.response?.durationMs, + requestHeaders = call.request.headers, + responseHeaders = when (val response = call.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.headers + else -> null + }, + requestBody = call.request.body, + responseBody = when (val response = call.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.body + else -> null + }, + contentType = when (val response = call.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.contentType + else -> null + }, + deviceId = call.appInstance.deviceId, + appInstance = call.appInstance.appInstance, + ) + } + + val response = NetworkLogsExportResponse( + data = exportedCalls, + metadata = ExportMetadata( + exportedAt = System.currentTimeMillis(), + totalItems = exportedCalls.size, + filteredBy = if (deviceId != null || startTimestamp != null || endTimestamp != null) { + FilterCriteria( + deviceId = deviceId, + startTimestamp = startTimestamp, + endTimestamp = endTimestamp, + ) + } else { + null + }, + ), + ) + + call.respond(HttpStatusCode.OK, response) + } catch (e: Exception) { + Logger.e("Error exporting network logs", e) + call.respond( + HttpStatusCode.InternalServerError, + mapOf("error" to (e.message ?: "Unknown error")) + ) + } + } +} \ No newline at end of file diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt new file mode 100644 index 000000000..09db93942 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt @@ -0,0 +1,37 @@ +package io.github.openflocon.domain.network.usecase + +import io.github.openflocon.domain.common.Either +import io.github.openflocon.domain.common.Failure +import io.github.openflocon.domain.common.Success +import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.repository.NetworkRepository + +class ExportNetworkLogsAsJsonUseCase( + private val networkRepository: NetworkRepository, +) { + suspend operator fun invoke( + deviceId: String? = null, + startTimestamp: Long? = null, + endTimestamp: Long? = null, + ): Either> { + return try { + val calls = networkRepository.getNetworkCalls() + + val filtered = calls.filter { call -> + val matchesDevice = deviceId == null || call.appInstance.deviceId == deviceId + val matchesStartTime = startTimestamp == null || call.request.startTime >= startTimestamp + val matchesEndTime = endTimestamp == null || call.request.startTime <= endTimestamp + + matchesDevice && matchesStartTime && matchesEndTime + } + + if (filtered.isEmpty()) { + return Failure(Throwable("No network logs found matching the criteria")) + } + + Success(filtered) + } catch (e: Exception) { + Failure(e) + } + } +} \ No newline at end of file From 971053cd308a8b3bb6db145f5907faf31a6c56f5 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:24:28 +0500 Subject: [PATCH 2/8] refactor: Update ServerJvm to include NetworkRepository and integrate network export endpoint - Add NetworkRepository as a dependency to ServerJvm - Update Server.desktop.kt to pass networkRepository to ServerJvm constructor - Integrate networkExportRoutes into the HTTP routing - Update HTTP server initialization to include the new export endpoint --- .../kotlin/com/flocon/data/remote/server/Server.kt | 6 +++++- .../kotlin/com/flocon/data/remote/server/Server.desktop.kt | 6 +++++- .../kotlin/com/flocon/data/remote/server/ServerJvm.kt | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt index 2164a89e1..1eed0ca4f 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt @@ -5,6 +5,7 @@ import com.flocon.data.remote.models.FloconIncomingMessageDataModel import com.flocon.data.remote.models.FloconOutgoingMessageDataModel import io.github.openflocon.domain.Constant import io.github.openflocon.domain.messages.models.FloconReceivedFileDomainModel +import io.github.openflocon.domain.network.repository.NetworkRepository import kotlinx.coroutines.flow.Flow import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi @@ -31,4 +32,7 @@ interface Server { @OptIn(ExperimentalUuidApi::class) fun newRequestId(): String = Uuid.random().toString() -expect fun getServer(json: Json): Server +expect fun getServer( + json: Json, + networkRepository: NetworkRepository, +): Server diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt index e092fdd03..d516c8ba7 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt @@ -1,5 +1,9 @@ package com.flocon.data.remote.server +import io.github.openflocon.domain.network.repository.NetworkRepository import kotlinx.serialization.json.Json -actual fun getServer(json: Json): Server = ServerJvm(json) +actual fun getServer( + json: Json, + networkRepository: NetworkRepository, +): Server = ServerJvm(json, networkRepository) diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt index ac443e2c4..8e89e2a51 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt @@ -5,6 +5,7 @@ import com.flocon.data.remote.models.FloconDeviceIdAndPackageNameDataModel import com.flocon.data.remote.models.FloconIncomingMessageDataModel import com.flocon.data.remote.models.FloconOutgoingMessageDataModel import io.github.openflocon.domain.messages.models.FloconReceivedFileDomainModel +import io.github.openflocon.domain.network.repository.NetworkRepository import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.http.content.streamProvider @@ -41,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds class ServerJvm( private val json: Json, + private val networkRepository: NetworkRepository, ) : Server { private val _receivedMessages = Channel() override val receivedMessages = _receivedMessages.receiveAsFlow() @@ -251,6 +253,11 @@ class ServerJvm( call.respondText("file received : ${savedFile?.absolutePath ?: "inconnu"}") } + + // Add network export routes + networkExportRoutes( + getNetworkCalls = { networkRepository.getNetworkCalls() } + ) } }.start(wait = false) From 73d2f670e198f2291038d7f1922912155809b4d4 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:26:43 +0500 Subject: [PATCH 3/8] refactor: Update DI module to pass networkRepository to Server constructor --- .../commonMain/kotlin/com/flocon/data/remote/messages/DI.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt index da6549c1b..1c6364ac9 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt @@ -11,7 +11,8 @@ import org.koin.dsl.module internal val messagesModule = module { single { getServer( - json = get() + json = get(), + networkRepository = get() ) } singleOf(::MessageRemoteDataSourceImpl) bind MessageRemoteDataSource::class From be0d071c2d634c62047652df1eb410d13ec87785 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:53:31 +0500 Subject: [PATCH 4/8] fix: Update ExportNetworkLogsAsJsonUseCase to use correct NetworkRepository methods --- .../usecase/ExportNetworkLogsAsJsonUseCase.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt index 09db93942..42d160c0a 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt @@ -3,19 +3,33 @@ package io.github.openflocon.domain.network.usecase import io.github.openflocon.domain.common.Either import io.github.openflocon.domain.common.Failure import io.github.openflocon.domain.common.Success +import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkFilterDomainModel import io.github.openflocon.domain.network.repository.NetworkRepository class ExportNetworkLogsAsJsonUseCase( private val networkRepository: NetworkRepository, ) { suspend operator fun invoke( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, deviceId: String? = null, startTimestamp: Long? = null, endTimestamp: Long? = null, ): Either> { return try { - val calls = networkRepository.getNetworkCalls() + val filter = NetworkFilterDomainModel( + statusCode = null, + contentType = null, + hasResponse = null, + searchText = null, + requestDate = null, + ) + val calls = networkRepository.getRequests( + deviceIdAndPackageName = deviceIdAndPackageName, + sortedBy = null, + filter = filter, + ) val filtered = calls.filter { call -> val matchesDevice = deviceId == null || call.appInstance.deviceId == deviceId @@ -34,4 +48,4 @@ class ExportNetworkLogsAsJsonUseCase( Failure(e) } } -} \ No newline at end of file +} From 79c4581557100e300c4b4cd26c9e4824cdc7aa23 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:59:39 +0500 Subject: [PATCH 5/8] fix: Update ExportNetworkLogsAsJsonUseCase with correct NetworkFilterDomainModel parameters --- .../usecase/ExportNetworkLogsAsJsonUseCase.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt index 42d160c0a..9abf8f524 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt @@ -13,17 +13,16 @@ class ExportNetworkLogsAsJsonUseCase( ) { suspend operator fun invoke( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, - deviceId: String? = null, + filterText: String? = null, startTimestamp: Long? = null, endTimestamp: Long? = null, ): Either> { return try { val filter = NetworkFilterDomainModel( - statusCode = null, - contentType = null, - hasResponse = null, - searchText = null, - requestDate = null, + filterOnAllColumns = filterText, + textsFilters = null, + methodFilter = null, + displayOldSessions = true, ) val calls = networkRepository.getRequests( deviceIdAndPackageName = deviceIdAndPackageName, @@ -32,11 +31,10 @@ class ExportNetworkLogsAsJsonUseCase( ) val filtered = calls.filter { call -> - val matchesDevice = deviceId == null || call.appInstance.deviceId == deviceId val matchesStartTime = startTimestamp == null || call.request.startTime >= startTimestamp val matchesEndTime = endTimestamp == null || call.request.startTime <= endTimestamp - matchesDevice && matchesStartTime && matchesEndTime + matchesStartTime && matchesEndTime } if (filtered.isEmpty()) { From 70f0d70952e7441a9f0495909554f12fd9a72921 Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:56:41 +0500 Subject: [PATCH 6/8] feat: Add network logs export HTTP API with three endpoints - Add GET /api/network-logs to fetch all captured network calls across all devices - Add GET /api/network-logs/{deviceId} to fetch calls for a specific device - Add GET /api/network-logs/{deviceId}/filter?startTimestamp=&endTimestamp= for time-range filtering - Add getAllRequests/getAllRequestsByDevice DAO queries reading deviceId directly from DB - Propagate getAllNetworkCalls(deviceId?) through NetworkLocalDataSource, NetworkRepository, and NetworkRepositoryImpl - Fix Koin circular dependency (NetworkRepositoryImpl -> NetworkRemoteDataSourceImpl -> Server -> NetworkRepository) by using Lazy in ServerJvm - Use respondText with manual JSON serialization to avoid requiring ContentNegotiation plugin - Fix deprecated dayOfMonth/monthNumber in TimeFormatter Co-Authored-By: Claude Sonnet 4.5 --- .../datasource/NetworkLocalDataSource.kt | 3 + .../repository/NetworkRepositoryImpl.kt | 5 + .../local/network/dao/FloconNetworkDao.kt | 12 ++ .../datasource/NetworkLocalDataSourceRoom.kt | 11 ++ .../com/flocon/data/remote/messages/DI.kt | 2 +- .../com/flocon/data/remote/server/Server.kt | 2 +- .../remote/server/NetworkExportEndpoint.kt | 173 ++++++++++-------- .../data/remote/server/Server.desktop.kt | 2 +- .../flocon/data/remote/server/ServerJvm.kt | 7 +- .../domain/common/time/TimeFormatter.kt | 4 +- .../network/repository/NetworkRepository.kt | 3 + 11 files changed, 144 insertions(+), 80 deletions(-) diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt index 5d64a80f5..770602b12 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt @@ -54,5 +54,8 @@ interface NetworkLocalDataSource { suspend fun deleteRequestOnDifferentSession(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) + // Returns (deviceId, call) pairs so callers can know which device each call belongs to + suspend fun getAllRequests(deviceId: String? = null): List> + suspend fun clear() } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt index 372050076..ff8f91d18 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt @@ -98,6 +98,11 @@ class NetworkRepositoryImpl( ) } + override suspend fun getAllNetworkCalls(deviceId: String?): List> = + withContext(dispatcherProvider.data) { + networkLocalDataSource.getAllRequests(deviceId) + } + override suspend fun getRequests( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, ids: List diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt index 0150cc5de..da7b39726 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt @@ -68,6 +68,18 @@ interface FloconNetworkDao { callId: String, ): FloconNetworkCallEntity? + @Query("SELECT * FROM FloconNetworkCallEntity ORDER BY request_startTime ASC") + suspend fun getAllRequests(): List + + @Query( + """ + SELECT * FROM FloconNetworkCallEntity + WHERE deviceId = :deviceId + ORDER BY request_startTime ASC + """, + ) + suspend fun getAllRequestsByDevice(deviceId: String): List + @Query("DELETE FROM FloconNetworkCallEntity") suspend fun clearAll() diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt index 42b896814..62122d379 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt @@ -260,6 +260,17 @@ class NetworkLocalDataSourceRoom( ) } + override suspend fun getAllRequests(deviceId: String?): List> { + val entities = if (deviceId != null) { + floconNetworkDao.getAllRequestsByDevice(deviceId) + } else { + floconNetworkDao.getAllRequests() + } + return entities.mapNotNull { entity -> + entity.toDomainModel()?.let { domain -> entity.deviceId to domain } + } + } + override suspend fun clear() { floconNetworkDao.clearAll() } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt index 1c6364ac9..f37c4a41e 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/messages/DI.kt @@ -12,7 +12,7 @@ internal val messagesModule = module { single { getServer( json = get(), - networkRepository = get() + networkRepository = lazy { get() }, ) } singleOf(::MessageRemoteDataSourceImpl) bind MessageRemoteDataSource::class diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt index 1eed0ca4f..074d46409 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/server/Server.kt @@ -34,5 +34,5 @@ fun newRequestId(): String = Uuid.random().toString() expect fun getServer( json: Json, - networkRepository: NetworkRepository, + networkRepository: Lazy, ): Server diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt index 5da655673..096e56932 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt @@ -6,89 +6,116 @@ import com.flocon.data.remote.models.FilterCriteria import com.flocon.data.remote.models.NetworkCallExport import com.flocon.data.remote.models.NetworkLogsExportResponse import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get +import io.ktor.server.response.respondText +import kotlinx.serialization.json.Json fun Route.networkExportRoutes( - getNetworkCalls: suspend () -> List, + json: Json, + getNetworkCalls: suspend (deviceId: String?) -> List>, ) { - get("/api/export/network-logs") { - try { - val deviceId = call.request.queryParameters["deviceId"] - val startTimestamp = call.request.queryParameters["startTimestamp"]?.toLongOrNull() - val endTimestamp = call.request.queryParameters["endTimestamp"]?.toLongOrNull() + // 1. GET /api/network-logs — all calls from all devices + get("/api/network-logs") { + val calls = getNetworkCalls(null) + call.respondJson(json, calls, deviceId = null, startTimestamp = null, endTimestamp = null) + } - val allCalls = getNetworkCalls() - - val filtered = allCalls.filter { call -> - val matchesDevice = deviceId == null || call.appInstance.deviceId == deviceId - val matchesStartTime = startTimestamp == null || call.request.startTime >= startTimestamp - val matchesEndTime = endTimestamp == null || call.request.startTime <= endTimestamp - - matchesDevice && matchesStartTime && matchesEndTime - } + // 2. GET /api/network-logs/{deviceId} — all calls for a specific device + get("/api/network-logs/{deviceId}") { + val deviceId = call.parameters["deviceId"] + val calls = getNetworkCalls(deviceId) + call.respondJson(json, calls, deviceId = deviceId, startTimestamp = null, endTimestamp = null) + } - val exportedCalls = filtered.map { call -> - NetworkCallExport( - callId = call.callId, - method = call.request.method, - url = call.request.url, - startTime = call.request.startTime, - startTimeFormatted = call.request.startTimeFormatted, - statusCode = when (val response = call.response) { - is FloconNetworkCallDomainModel.Response.Success -> - when (val info = response.specificInfos) { - is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> info.httpCode - else -> null - } - else -> null - }, - durationMs = call.response?.durationMs, - requestHeaders = call.request.headers, - responseHeaders = when (val response = call.response) { - is FloconNetworkCallDomainModel.Response.Success -> response.headers - else -> null - }, - requestBody = call.request.body, - responseBody = when (val response = call.response) { - is FloconNetworkCallDomainModel.Response.Success -> response.body - else -> null - }, - contentType = when (val response = call.response) { - is FloconNetworkCallDomainModel.Response.Success -> response.contentType - else -> null - }, - deviceId = call.appInstance.deviceId, - appInstance = call.appInstance.appInstance, - ) - } + // 3. GET /api/network-logs/{deviceId}?startTimestamp=&endTimestamp= — filtered by device + time range + get("/api/network-logs/{deviceId}/filter") { + val deviceId = call.parameters["deviceId"] + val startTimestamp = call.request.queryParameters["startTimestamp"]?.toLongOrNull() + val endTimestamp = call.request.queryParameters["endTimestamp"]?.toLongOrNull() - val response = NetworkLogsExportResponse( - data = exportedCalls, - metadata = ExportMetadata( - exportedAt = System.currentTimeMillis(), - totalItems = exportedCalls.size, - filteredBy = if (deviceId != null || startTimestamp != null || endTimestamp != null) { - FilterCriteria( - deviceId = deviceId, - startTimestamp = startTimestamp, - endTimestamp = endTimestamp, - ) - } else { - null - }, - ), - ) + val calls = getNetworkCalls(deviceId) + call.respondJson(json, calls, deviceId = deviceId, startTimestamp = startTimestamp, endTimestamp = endTimestamp) + } +} - call.respond(HttpStatusCode.OK, response) - } catch (e: Exception) { - Logger.e("Error exporting network logs", e) - call.respond( - HttpStatusCode.InternalServerError, - mapOf("error" to (e.message ?: "Unknown error")) +private suspend fun io.ktor.server.application.ApplicationCall.respondJson( + json: Json, + calls: List>, + deviceId: String?, + startTimestamp: Long?, + endTimestamp: Long?, +) { + try { + val filtered = calls.filter { (_, networkCall) -> + val matchesStart = startTimestamp == null || networkCall.request.startTime >= startTimestamp + val matchesEnd = endTimestamp == null || networkCall.request.startTime <= endTimestamp + matchesStart && matchesEnd + } + + val exportedCalls = filtered.map { (storedDeviceId, networkCall) -> + NetworkCallExport( + callId = networkCall.callId, + method = networkCall.request.method, + url = networkCall.request.url, + startTime = networkCall.request.startTime, + startTimeFormatted = networkCall.request.startTimeFormatted, + statusCode = when (val response = networkCall.response) { + is FloconNetworkCallDomainModel.Response.Success -> + when (val info = response.specificInfos) { + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> info.httpCode + else -> null + } + else -> null + }, + durationMs = networkCall.response?.durationMs, + requestHeaders = networkCall.request.headers, + responseHeaders = when (val response = networkCall.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.headers + else -> null + }, + requestBody = networkCall.request.body, + responseBody = when (val response = networkCall.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.body + else -> null + }, + contentType = when (val response = networkCall.response) { + is FloconNetworkCallDomainModel.Response.Success -> response.contentType + else -> null + }, + deviceId = storedDeviceId, + appInstance = networkCall.appInstance, ) } + + val response = NetworkLogsExportResponse( + data = exportedCalls, + metadata = ExportMetadata( + exportedAt = System.currentTimeMillis(), + totalItems = exportedCalls.size, + filteredBy = if (deviceId != null || startTimestamp != null || endTimestamp != null) { + FilterCriteria( + deviceId = deviceId, + startTimestamp = startTimestamp, + endTimestamp = endTimestamp, + ) + } else null, + ), + ) + + respondText( + json.encodeToString(NetworkLogsExportResponse.serializer(), response), + ContentType.Application.Json, + HttpStatusCode.OK, + ) + } catch (e: Exception) { + Logger.e("Error exporting network logs", e) + respondText( + """{"error":"${e.message?.replace("\"", "\\\"")}"}""", + ContentType.Application.Json, + HttpStatusCode.InternalServerError, + ) } -} \ No newline at end of file +} diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt index d516c8ba7..ca9649e40 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/Server.desktop.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.json.Json actual fun getServer( json: Json, - networkRepository: NetworkRepository, + networkRepository: Lazy, ): Server = ServerJvm(json, networkRepository) diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt index 8e89e2a51..2da1be563 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt @@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.seconds class ServerJvm( private val json: Json, - private val networkRepository: NetworkRepository, + private val networkRepository: Lazy, ) : Server { private val _receivedMessages = Channel() override val receivedMessages = _receivedMessages.receiveAsFlow() @@ -256,7 +256,10 @@ class ServerJvm( // Add network export routes networkExportRoutes( - getNetworkCalls = { networkRepository.getNetworkCalls() } + json = json, + getNetworkCalls = { deviceId -> + networkRepository.value.getAllNetworkCalls(deviceId) + } ) } }.start(wait = false) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt index 222dfe28e..af5dc7052 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt @@ -29,8 +29,8 @@ fun formatDate(timestamp: Long): String { val instant = Instant.fromEpochMilliseconds(timestamp) val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) - val day = localDateTime.dayOfMonth - val month = localDateTime.monthNumber + val day = localDateTime.day + val month = localDateTime.month.number val year = localDateTime.year val hours = localDateTime.hour diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt index 56e7dc8e2..f775fbba3 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt @@ -53,6 +53,9 @@ interface NetworkRepository { deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel ) + // Returns (deviceId, call) pairs — deviceId comes from the stored record + suspend fun getAllNetworkCalls(deviceId: String? = null): List> + suspend fun replayRequest( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, request: FloconNetworkCallDomainModel From 17be260e1de41f61c9449d20bb1dcef11a9905ef Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:19:53 +0500 Subject: [PATCH 7/8] fix: Push timestamp filtering to DB and address PR review comments - Add startTimestamp/endTimestamp params to FloconNetworkDao queries so time-range filtering happens at DB level instead of in memory, preventing high memory usage on large datasets - Thread startTimestamp/endTimestamp through NetworkLocalDataSource, NetworkLocalDataSourceRoom, NetworkRepository, and NetworkRepositoryImpl - Remove in-memory timestamp filter from NetworkExportEndpoint - Replace fragile string-interpolated error JSON with MapSerializer to safely handle special characters in error messages - Revert TimeFormatter to dayOfMonth/monthNumber as day/month.number do not exist in the project's kotlinx.datetime version Co-Authored-By: Claude Sonnet 4.5 --- .../datasource/NetworkLocalDataSource.kt | 6 +++- .../repository/NetworkRepositoryImpl.kt | 8 +++-- .../local/network/dao/FloconNetworkDao.kt | 22 +++++++++++-- .../datasource/NetworkLocalDataSourceRoom.kt | 10 ++++-- .../remote/server/NetworkExportEndpoint.kt | 31 ++++++++++--------- .../flocon/data/remote/server/ServerJvm.kt | 4 +-- .../domain/common/time/TimeFormatter.kt | 4 +-- .../network/repository/NetworkRepository.kt | 6 +++- 8 files changed, 62 insertions(+), 29 deletions(-) diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt index 770602b12..82fcef17b 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt @@ -55,7 +55,11 @@ interface NetworkLocalDataSource { suspend fun deleteRequestOnDifferentSession(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) // Returns (deviceId, call) pairs so callers can know which device each call belongs to - suspend fun getAllRequests(deviceId: String? = null): List> + suspend fun getAllRequests( + deviceId: String? = null, + startTimestamp: Long? = null, + endTimestamp: Long? = null, + ): List> suspend fun clear() } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt index ff8f91d18..af5041e62 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt @@ -98,9 +98,13 @@ class NetworkRepositoryImpl( ) } - override suspend fun getAllNetworkCalls(deviceId: String?): List> = + override suspend fun getAllNetworkCalls( + deviceId: String?, + startTimestamp: Long?, + endTimestamp: Long?, + ): List> = withContext(dispatcherProvider.data) { - networkLocalDataSource.getAllRequests(deviceId) + networkLocalDataSource.getAllRequests(deviceId, startTimestamp, endTimestamp) } override suspend fun getRequests( diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt index da7b39726..0e9234169 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/dao/FloconNetworkDao.kt @@ -68,17 +68,33 @@ interface FloconNetworkDao { callId: String, ): FloconNetworkCallEntity? - @Query("SELECT * FROM FloconNetworkCallEntity ORDER BY request_startTime ASC") - suspend fun getAllRequests(): List + @Query( + """ + SELECT * FROM FloconNetworkCallEntity + WHERE (:startTimestamp IS NULL OR request_startTime >= :startTimestamp) + AND (:endTimestamp IS NULL OR request_startTime <= :endTimestamp) + ORDER BY request_startTime ASC + """, + ) + suspend fun getAllRequests( + startTimestamp: Long? = null, + endTimestamp: Long? = null, + ): List @Query( """ SELECT * FROM FloconNetworkCallEntity WHERE deviceId = :deviceId + AND (:startTimestamp IS NULL OR request_startTime >= :startTimestamp) + AND (:endTimestamp IS NULL OR request_startTime <= :endTimestamp) ORDER BY request_startTime ASC """, ) - suspend fun getAllRequestsByDevice(deviceId: String): List + suspend fun getAllRequestsByDevice( + deviceId: String, + startTimestamp: Long? = null, + endTimestamp: Long? = null, + ): List @Query("DELETE FROM FloconNetworkCallEntity") suspend fun clearAll() diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt index 62122d379..aaddcda46 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt @@ -260,11 +260,15 @@ class NetworkLocalDataSourceRoom( ) } - override suspend fun getAllRequests(deviceId: String?): List> { + override suspend fun getAllRequests( + deviceId: String?, + startTimestamp: Long?, + endTimestamp: Long?, + ): List> { val entities = if (deviceId != null) { - floconNetworkDao.getAllRequestsByDevice(deviceId) + floconNetworkDao.getAllRequestsByDevice(deviceId, startTimestamp, endTimestamp) } else { - floconNetworkDao.getAllRequests() + floconNetworkDao.getAllRequests(startTimestamp, endTimestamp) } return entities.mapNotNull { entity -> entity.toDomainModel()?.let { domain -> entity.deviceId to domain } diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt index 096e56932..59c459e09 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt @@ -8,40 +8,44 @@ import com.flocon.data.remote.models.NetworkLogsExportResponse import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respondText import io.ktor.server.routing.Route import io.ktor.server.routing.get -import io.ktor.server.response.respondText +import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer fun Route.networkExportRoutes( json: Json, - getNetworkCalls: suspend (deviceId: String?) -> List>, + // Filtering is done at DB level — timestamps passed through to the query + getNetworkCalls: suspend (deviceId: String?, startTimestamp: Long?, endTimestamp: Long?) -> List>, ) { // 1. GET /api/network-logs — all calls from all devices get("/api/network-logs") { - val calls = getNetworkCalls(null) + val calls = getNetworkCalls(null, null, null) call.respondJson(json, calls, deviceId = null, startTimestamp = null, endTimestamp = null) } // 2. GET /api/network-logs/{deviceId} — all calls for a specific device get("/api/network-logs/{deviceId}") { val deviceId = call.parameters["deviceId"] - val calls = getNetworkCalls(deviceId) + val calls = getNetworkCalls(deviceId, null, null) call.respondJson(json, calls, deviceId = deviceId, startTimestamp = null, endTimestamp = null) } - // 3. GET /api/network-logs/{deviceId}?startTimestamp=&endTimestamp= — filtered by device + time range + // 3. GET /api/network-logs/{deviceId}/filter?startTimestamp=&endTimestamp= — device + time range get("/api/network-logs/{deviceId}/filter") { val deviceId = call.parameters["deviceId"] val startTimestamp = call.request.queryParameters["startTimestamp"]?.toLongOrNull() val endTimestamp = call.request.queryParameters["endTimestamp"]?.toLongOrNull() - val calls = getNetworkCalls(deviceId) + val calls = getNetworkCalls(deviceId, startTimestamp, endTimestamp) call.respondJson(json, calls, deviceId = deviceId, startTimestamp = startTimestamp, endTimestamp = endTimestamp) } } -private suspend fun io.ktor.server.application.ApplicationCall.respondJson( +private suspend fun ApplicationCall.respondJson( json: Json, calls: List>, deviceId: String?, @@ -49,13 +53,7 @@ private suspend fun io.ktor.server.application.ApplicationCall.respondJson( endTimestamp: Long?, ) { try { - val filtered = calls.filter { (_, networkCall) -> - val matchesStart = startTimestamp == null || networkCall.request.startTime >= startTimestamp - val matchesEnd = endTimestamp == null || networkCall.request.startTime <= endTimestamp - matchesStart && matchesEnd - } - - val exportedCalls = filtered.map { (storedDeviceId, networkCall) -> + val exportedCalls = calls.map { (storedDeviceId, networkCall) -> NetworkCallExport( callId = networkCall.callId, method = networkCall.request.method, @@ -113,7 +111,10 @@ private suspend fun io.ktor.server.application.ApplicationCall.respondJson( } catch (e: Exception) { Logger.e("Error exporting network logs", e) respondText( - """{"error":"${e.message?.replace("\"", "\\\"")}"}""", + json.encodeToString( + MapSerializer(serializer(), serializer()), + mapOf("error" to (e.message ?: "Unknown error")), + ), ContentType.Application.Json, HttpStatusCode.InternalServerError, ) diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt index 2da1be563..f6301bc42 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/ServerJvm.kt @@ -257,8 +257,8 @@ class ServerJvm( // Add network export routes networkExportRoutes( json = json, - getNetworkCalls = { deviceId -> - networkRepository.value.getAllNetworkCalls(deviceId) + getNetworkCalls = { deviceId, startTimestamp, endTimestamp -> + networkRepository.value.getAllNetworkCalls(deviceId, startTimestamp, endTimestamp) } ) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt index af5dc7052..222dfe28e 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/common/time/TimeFormatter.kt @@ -29,8 +29,8 @@ fun formatDate(timestamp: Long): String { val instant = Instant.fromEpochMilliseconds(timestamp) val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) - val day = localDateTime.day - val month = localDateTime.month.number + val day = localDateTime.dayOfMonth + val month = localDateTime.monthNumber val year = localDateTime.year val hours = localDateTime.hour diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt index f775fbba3..f5d5e0f74 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt @@ -54,7 +54,11 @@ interface NetworkRepository { ) // Returns (deviceId, call) pairs — deviceId comes from the stored record - suspend fun getAllNetworkCalls(deviceId: String? = null): List> + suspend fun getAllNetworkCalls( + deviceId: String? = null, + startTimestamp: Long? = null, + endTimestamp: Long? = null, + ): List> suspend fun replayRequest( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, From 4a59893725936f365a6a1b6e089f533969fa941d Mon Sep 17 00:00:00 2001 From: Abdullah Sohail <90067650+abdullahsohailcs@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:28:27 +0500 Subject: [PATCH 8/8] fix: address PR review comments on network logs export - Replace Pair with NetworkCallWithDeviceId model - Mark NetworkLogsExportModel classes as internal - Mark ExportNetworkLogsAsJsonUseCase constructor as internal Co-Authored-By: Claude Sonnet 4.6 --- .../core/network/datasource/NetworkLocalDataSource.kt | 4 ++-- .../core/network/repository/NetworkRepositoryImpl.kt | 3 ++- .../network/datasource/NetworkLocalDataSourceRoom.kt | 7 +++++-- .../flocon/data/remote/models/NetworkLogsExportModel.kt | 8 ++++---- .../flocon/data/remote/server/NetworkExportEndpoint.kt | 9 ++++++--- .../domain/network/models/NetworkCallWithDeviceId.kt | 6 ++++++ .../domain/network/repository/NetworkRepository.kt | 4 ++-- .../network/usecase/ExportNetworkLogsAsJsonUseCase.kt | 2 +- 8 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/NetworkCallWithDeviceId.kt diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt index 82fcef17b..6ff38c03d 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkLocalDataSource.kt @@ -3,6 +3,7 @@ package io.github.openflocon.data.core.network.datasource import androidx.paging.PagingData import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId import io.github.openflocon.domain.network.models.NetworkFilterDomainModel import io.github.openflocon.domain.network.models.NetworkSortDomainModel import kotlinx.coroutines.flow.Flow @@ -54,12 +55,11 @@ interface NetworkLocalDataSource { suspend fun deleteRequestOnDifferentSession(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) - // Returns (deviceId, call) pairs so callers can know which device each call belongs to suspend fun getAllRequests( deviceId: String? = null, startTimestamp: Long? = null, endTimestamp: Long? = null, - ): List> + ): List suspend fun clear() } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt index af5041e62..d7728865f 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt @@ -15,6 +15,7 @@ import io.github.openflocon.domain.messages.repository.MessagesReceiverRepositor import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.BadQualityConfigId import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId import io.github.openflocon.domain.network.models.FloconNetworkResponseOnlyDomainModel import io.github.openflocon.domain.network.models.MockNetworkDomainModel import io.github.openflocon.domain.network.models.NetworkFilterDomainModel @@ -102,7 +103,7 @@ class NetworkRepositoryImpl( deviceId: String?, startTimestamp: Long?, endTimestamp: Long?, - ): List> = + ): List = withContext(dispatcherProvider.data) { networkLocalDataSource.getAllRequests(deviceId, startTimestamp, endTimestamp) } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt index aaddcda46..858244eb6 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/datasource/NetworkLocalDataSourceRoom.kt @@ -12,6 +12,7 @@ import io.github.openflocon.data.local.network.mapper.toEntity import io.github.openflocon.data.local.network.models.FloconNetworkCallEntity import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId import io.github.openflocon.domain.network.models.NetworkFilterDomainModel import io.github.openflocon.domain.network.models.NetworkSortDomainModel import io.github.openflocon.domain.network.models.NetworkTextFilterColumns @@ -264,14 +265,16 @@ class NetworkLocalDataSourceRoom( deviceId: String?, startTimestamp: Long?, endTimestamp: Long?, - ): List> { + ): List { val entities = if (deviceId != null) { floconNetworkDao.getAllRequestsByDevice(deviceId, startTimestamp, endTimestamp) } else { floconNetworkDao.getAllRequests(startTimestamp, endTimestamp) } return entities.mapNotNull { entity -> - entity.toDomainModel()?.let { domain -> entity.deviceId to domain } + entity.toDomainModel()?.let { domain -> + NetworkCallWithDeviceId(deviceId = entity.deviceId, call = domain) + } } } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt index 682f05170..ac4c8b607 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/models/NetworkLogsExportModel.kt @@ -3,27 +3,27 @@ package com.flocon.data.remote.models import kotlinx.serialization.Serializable @Serializable -data class NetworkLogsExportResponse( +internal data class NetworkLogsExportResponse( val data: List, val metadata: ExportMetadata, ) @Serializable -data class ExportMetadata( +internal data class ExportMetadata( val exportedAt: Long, val totalItems: Int, val filteredBy: FilterCriteria?, ) @Serializable -data class FilterCriteria( +internal data class FilterCriteria( val deviceId: String? = null, val startTimestamp: Long? = null, val endTimestamp: Long? = null, ) @Serializable -data class NetworkCallExport( +internal data class NetworkCallExport( val callId: String, val method: String, val url: String, diff --git a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt index 59c459e09..776aee751 100644 --- a/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt +++ b/FloconDesktop/data/remote/src/desktopMain/kotlin/com/flocon/data/remote/server/NetworkExportEndpoint.kt @@ -6,6 +6,7 @@ import com.flocon.data.remote.models.FilterCriteria import com.flocon.data.remote.models.NetworkCallExport import com.flocon.data.remote.models.NetworkLogsExportResponse import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall @@ -19,7 +20,7 @@ import kotlinx.serialization.serializer fun Route.networkExportRoutes( json: Json, // Filtering is done at DB level — timestamps passed through to the query - getNetworkCalls: suspend (deviceId: String?, startTimestamp: Long?, endTimestamp: Long?) -> List>, + getNetworkCalls: suspend (deviceId: String?, startTimestamp: Long?, endTimestamp: Long?) -> List, ) { // 1. GET /api/network-logs — all calls from all devices get("/api/network-logs") { @@ -47,13 +48,15 @@ fun Route.networkExportRoutes( private suspend fun ApplicationCall.respondJson( json: Json, - calls: List>, + calls: List, deviceId: String?, startTimestamp: Long?, endTimestamp: Long?, ) { try { - val exportedCalls = calls.map { (storedDeviceId, networkCall) -> + val exportedCalls = calls.map { networkCallWithDeviceId -> + val networkCall = networkCallWithDeviceId.call + val storedDeviceId = networkCallWithDeviceId.deviceId NetworkCallExport( callId = networkCall.callId, method = networkCall.request.method, diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/NetworkCallWithDeviceId.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/NetworkCallWithDeviceId.kt new file mode 100644 index 000000000..600d9edaf --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/NetworkCallWithDeviceId.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.domain.network.models + +data class NetworkCallWithDeviceId( + val deviceId: String, + val call: FloconNetworkCallDomainModel, +) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt index f5d5e0f74..58c59d6c7 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/repository/NetworkRepository.kt @@ -3,6 +3,7 @@ package io.github.openflocon.domain.network.repository import androidx.paging.PagingData import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel +import io.github.openflocon.domain.network.models.NetworkCallWithDeviceId import io.github.openflocon.domain.network.models.NetworkFilterDomainModel import io.github.openflocon.domain.network.models.NetworkSortDomainModel import kotlinx.coroutines.flow.Flow @@ -53,12 +54,11 @@ interface NetworkRepository { deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel ) - // Returns (deviceId, call) pairs — deviceId comes from the stored record suspend fun getAllNetworkCalls( deviceId: String? = null, startTimestamp: Long? = null, endTimestamp: Long? = null, - ): List> + ): List suspend fun replayRequest( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt index 9abf8f524..7ebe79149 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkLogsAsJsonUseCase.kt @@ -8,7 +8,7 @@ import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.NetworkFilterDomainModel import io.github.openflocon.domain.network.repository.NetworkRepository -class ExportNetworkLogsAsJsonUseCase( +class ExportNetworkLogsAsJsonUseCase internal constructor( private val networkRepository: NetworkRepository, ) { suspend operator fun invoke(