feat: FEAT-05 — DTC history sync to remote server
- Server: dtc_events table (migrate_phase27_dtc.sql), db.py helpers (upsert_dtc_snapshot, list_dtc_events, delete_dtc_event) - Server API: POST /api/v1/dtc/batch (device auth), GET/DELETE /api/v1/portal/dtc - Server portal: /dtc page with code, description, MIL, status, timestamps, delete - Server nav: DTC history in sidebar + mobile bottom nav - Pi: _upload_dtc_snapshot() in app_upload.py — uploads when snapshot changes, skips when OBD disconnected or unchanged
This commit is contained in:
@@ -34,12 +34,12 @@ data class DownloadProgress(
|
||||
|
||||
class DashcamViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val baseUrl = "http://10.42.0.1/"
|
||||
private val baseUrl = "http://10.42.0.1" // no trailing slash
|
||||
private val okHttp = OkHttpClient.Builder().build()
|
||||
private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true }
|
||||
|
||||
private val api: PiApi = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.baseUrl("$baseUrl/") // Retrofit requires trailing slash
|
||||
.client(okHttp)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
@@ -93,14 +93,14 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
val dir = app.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: return@launch
|
||||
val localFile = File(dir, video.path.substringAfterLast("/"))
|
||||
|
||||
// Skip if already downloaded
|
||||
// Skip if already downloaded and size matches
|
||||
if (localFile.exists() && localFile.length() == video.size) {
|
||||
_downloadEvents.emit("Already downloaded: ${localFile.name}")
|
||||
_downloadEvents.emit("✓ Already downloaded: ${localFile.name}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/local-video/file?path=${video.path}")
|
||||
.get()
|
||||
@@ -108,10 +108,10 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
val response = okHttp.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("HTTP ${response.code}")
|
||||
return@withContext "Download failed: HTTP ${response.code}"
|
||||
}
|
||||
|
||||
val body = response.body ?: throw Exception("Empty body")
|
||||
val body = response.body ?: return@withContext "Download failed: Empty body"
|
||||
val total = body.contentLength()
|
||||
localFile.parentFile?.mkdirs()
|
||||
|
||||
@@ -124,19 +124,19 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
if (read == -1L) break
|
||||
sink.write(buffer, read)
|
||||
downloaded += read
|
||||
_downloadProgress.value = _downloadProgress.value + (
|
||||
video.path to DownloadProgress(video.path, downloaded, total)
|
||||
)
|
||||
val progress = DownloadProgress(video.path, downloaded, total)
|
||||
_downloadProgress.value = _downloadProgress.value + (video.path to progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_downloadProgress.value = _downloadProgress.value - video.path
|
||||
_downloadEvents.emit("Downloaded: ${localFile.name}")
|
||||
"✓ Downloaded: ${localFile.name} (${downloaded} bytes)"
|
||||
}
|
||||
_downloadEvents.emit(result)
|
||||
} catch (e: Exception) {
|
||||
_downloadProgress.value = _downloadProgress.value - video.path
|
||||
_downloadEvents.emit("Download failed: ${e.message}")
|
||||
_downloadEvents.emit("✗ Download failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import com.yarisobd.app.data.remote.dto.GpsStatus
|
||||
import com.yarisobd.app.data.remote.dto.VideoListResponse
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Streaming
|
||||
|
||||
@@ -32,6 +32,6 @@ interface PiApi {
|
||||
suspend fun getThumbnail(@Query("path") path: String): ResponseBody
|
||||
|
||||
/** Delete local copy only */
|
||||
@DELETE("/api/local-video")
|
||||
@HTTP(method = "DELETE", path = "/api/local-video", hasBody = true)
|
||||
suspend fun deleteVideo(@Body body: Map<String, String>): Map<String, Any?>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user