From ed5e38bdb8728d27f082846d5f59d6b4fc3d7d4a Mon Sep 17 00:00:00 2001 From: TCFDONMEZ Date: Fri, 9 Oct 2026 23:49:57 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20lifetime=20optional=20in=20validation=20?= =?UTF-8?q?+=20type=20=E2=80=94=20app=20no=20longer=20sends=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateConsumption required lifetime, but the app stopped sending it (backend now derives it). asObject(undefined) threw 'body is required', rejecting every chunk. Now lifetime is optional in both the validator and the DailyConsumption type. Co-Authored-By: Claude --- .../evsuite/abrp/ConsumptionCloudClient.java | 23 ++++++++++--------- web/src/lib/mongo.ts | 3 ++- web/src/lib/validate.ts | 5 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/evsuite/abrp/ConsumptionCloudClient.java b/app/src/main/java/com/evsuite/abrp/ConsumptionCloudClient.java index 0ef1b9f..118d938 100644 --- a/app/src/main/java/com/evsuite/abrp/ConsumptionCloudClient.java +++ b/app/src/main/java/com/evsuite/abrp/ConsumptionCloudClient.java @@ -88,7 +88,7 @@ final class ConsumptionCloudClient { void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) { long elapsed = android.os.SystemClock.elapsedRealtime(); if (endpoint.isEmpty() || token.isEmpty()) { - Log.d(TAG, "Sync skipped: API not configured"); + Log.i(TAG, "Sync skipped: API not configured"); return; } syncPendingChargesIfDue(charges, elapsed); @@ -107,10 +107,10 @@ final class ConsumptionCloudClient { ConsumptionTracker.CloudSnapshot chunk = tracker.takeChunkDelta(); if (chunk == null) { - Log.d(TAG, "Sync due but no new consumption since last chunk"); + Log.i(TAG, "Sync due but no new consumption since last chunk"); return; // no driving since the last chunk } - Log.d(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km) + Log.i(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km) + "km, +" + String.format("%.2f", chunk.day.kwh) + "kWh"); try { JSONObject body = consumptionBody(chunk, nowMs); @@ -153,7 +153,7 @@ final class ConsumptionCloudClient { String date = body.optString("date", "?"); if (send("/v1/consumption/daily", "PUT", body)) { sent++; - Log.d(TAG, "Cached chunk for " + date + " sent"); + Log.i(TAG, "Cached chunk for " + date + " sent"); } else { // Server rejected — keep it, try the next one too. remaining.put(body); @@ -227,16 +227,16 @@ final class ConsumptionCloudClient { private void syncPendingCharges(ChargeSessionTracker charges, List pending) throws Exception { - Log.d(TAG, "Found " + pending.size() + " pending charge sessions"); + Log.i(TAG, "Found " + pending.size() + " pending charge sessions"); int sent = 0; for (JSONObject session : pending) { JSONObject body = new JSONObject(session.toString()).put("installationId", installationId); String id = session.getString("sessionId"); - Log.d(TAG, "Uploading charge session: " + id); + Log.i(TAG, "Uploading charge session: " + id); if (send("/v1/charging-sessions/" + id, "PUT", body)) { charges.markUploaded(id); logEvent(true, 200, "Şarj session gönderildi: " + id); - Log.d(TAG, "Successfully uploaded charge session: " + id); + Log.i(TAG, "Successfully uploaded charge session: " + id); } else { logEvent(false, 0, "Şarj session gönderilemedi: " + id); Log.w(TAG, "Failed to upload charge session: " + id); @@ -251,7 +251,8 @@ final class ConsumptionCloudClient { try { byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8); String url = trimSlash(endpoint) + path; - Log.d(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)"); + Log.i(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)"); + Log.i(TAG, "Body: " + body.toString()); connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(method); connection.setConnectTimeout(TIMEOUT_MS); @@ -262,7 +263,7 @@ final class ConsumptionCloudClient { connection.setRequestProperty("Authorization", "Bearer " + token); try (OutputStream out = connection.getOutputStream()) { out.write(bytes); } int status = connection.getResponseCode(); - Log.d(TAG, "Response: HTTP " + status); + Log.i(TAG, "Response: HTTP " + status); if (status < 200 || status >= 300) { String errBody = readErrorBody(connection); Log.w(TAG, "Sync rejected with HTTP " + status + (errBody.isEmpty() ? "" : ": " + errBody)); @@ -308,7 +309,7 @@ final class ConsumptionCloudClient { try { List fresh = fetchHistory(); if (!fresh.isEmpty()) cachedHistory = fresh; - Log.d(TAG, "Fetched " + fresh.size() + " history records from cloud"); + Log.i(TAG, "Fetched " + fresh.size() + " history records from cloud"); logEvent(true, 200, "History çekildi: " + fresh.size() + " gün"); } catch (Exception e) { String msg = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); @@ -363,7 +364,7 @@ final class ConsumptionCloudClient { connection.setReadTimeout(TIMEOUT_MS); connection.setRequestProperty("Authorization", "Bearer " + token); int status = connection.getResponseCode(); - Log.d(TAG, "History GET response: HTTP " + status); + Log.i(TAG, "History GET response: HTTP " + status); if (status < 200 || status >= 300) { String err = readErrorBody(connection); Log.w(TAG, "History fetch rejected with HTTP " + status + (err.isEmpty() ? "" : ": " + err)); diff --git a/web/src/lib/mongo.ts b/web/src/lib/mongo.ts index 230422c..508dcc6 100644 --- a/web/src/lib/mongo.ts +++ b/web/src/lib/mongo.ts @@ -22,7 +22,8 @@ export interface DailyConsumption { date: string; recordedAt: Date; day: Totals; - lifetime: Totals; + /** Optional — the backend derives lifetime as the sum of all days' totals. */ + lifetime?: Totals; sohPercent?: number; createdAt: Date; updatedAt: Date; diff --git a/web/src/lib/validate.ts b/web/src/lib/validate.ts index 2f9447a..1184fc0 100644 --- a/web/src/lib/validate.ts +++ b/web/src/lib/validate.ts @@ -21,12 +21,15 @@ export function validateConsumption(body: unknown): ConsumptionInput { const date = record.date; if (typeof date !== "string" || !ISO_DATE.test(date)) throw new ValidationError("date is invalid"); const sohPercent = record.sohPercent; + const lifetime = record.lifetime; return { installationId: validateUuid(record.installationId, "installationId"), date, recordedAt: validateDate(record.recordedAt, "recordedAt"), day: validateTotals(record.day, "day"), - lifetime: validateTotals(record.lifetime, "lifetime"), + // Lifetime is optional — the backend derives it from day totals, and the app no longer + // sends it. Legacy documents may still carry one; new chunks do not. + ...(lifetime === undefined ? {} : { lifetime: validateTotals(lifetime, "lifetime") }), ...(sohPercent === undefined ? {} : { sohPercent: bounded(sohPercent, 0, 100, "sohPercent") }), }; }