diff --git a/SPECS.md b/SPECS.md index 5d8f158..dff6558 100644 --- a/SPECS.md +++ b/SPECS.md @@ -74,7 +74,7 @@ car ──live telemetry───▶ car.stinnesbeck.com ──▶ ABRP api.ite - [x] Manifests in `k8s/`, validated offline. **Not applied** — that is Nils' step (`k8s/README.md`). - [x] Car app 2.3.0: Server tab with config import, live telemetry upload, cadence controls. Unit tests and lint pass; **not yet tried on the car**. -- [ ] Push the image (`mg4-server:1.1.1`) to git.stinnesbeck.com (needs a registry login on the build machine). +- [ ] Push the image (`mg4-server:1.1.2`) to git.stinnesbeck.com (needs a registry login on the build machine). - [ ] Check the Traefik ingress class in `k8s/ingress.yaml` (placeholder `traefik-external`). - [ ] First drive with 2.3.0: telemetry arrives in InfluxDB and at ABRP. @@ -90,9 +90,6 @@ car ──live telemetry───▶ car.stinnesbeck.com ──▶ ABRP api.ite (`ALLOWED_HOSTS`). For git.stinnesbeck.com it needs the host on the allowlist, and the response parsing checked against that server's releases API. Release assets must be named `NilsMG4-unstable-.apk` with a `.sha256` sidecar (`build-car` writes both). -- [ ] **Charging currency.** Prices and costs are shown in Turkish lira (`₺/kWh`, `₺`) in - `pane_charging.xml` and `MainActivity.java`. -- [ ] **Dashboard language.** The `web/` pages are still Turkish and show prices in lira. - [ ] **Telemetry queue survives restarts.** Unsent live samples are kept in memory only. - [ ] **Translate the rest.** The docs (`CHANGELOG.md`, `OTA.md`'s message table, comments in `mise.toml`/`app/build.gradle`) are still Turkish or French. @@ -116,6 +113,9 @@ car ──live telemetry───▶ car.stinnesbeck.com ──▶ ABRP api.ite telemetry restored from before `ea8032a` and sent to the server, Upload-tab cadence controls reconnected, dead switches removed. The removed ABRP classes' unit tests compile and pass again (AbrpApi and HeadUnitWeather tests deleted with their classes). +- 2026-10-17 — Accent colour: MG red instead of muted gold. Web app renamed to Nils' MG4. +- 2026-10-17 — Dashboard translated to English (en-GB formatting); euro instead of lira in the + dashboard and the car app's charging tab. - 2026-10-17 — Temporary on-screen diagnostics (b2a5b32) to debug blank values without adb; removed again (1002962) once values showed on the car. Revert 1002962 to bring them back. diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 76c4e64..7df5cbb 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -888,7 +888,7 @@ public class MainActivity extends AppCompatActivity { chargePriceInput.setText(String.format(java.util.Locale.US, "%.2f", value)); Toast.makeText(this, "Charging price saved", Toast.LENGTH_SHORT).show(); } catch (NumberFormatException e) { - chargePriceLayout.setError("Enter a valid ₺/kWh value"); + chargePriceLayout.setError("Enter a valid €/kWh value"); } } @@ -916,7 +916,7 @@ public class MainActivity extends AppCompatActivity { java.util.Locale.getDefault(), "%.2f kWh", session.gridEnergyKwh) : "—"); long sec = session.durationSeconds; chargeDuration.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%02d:%02d:%02d", sec/3600,(sec%3600)/60,sec%60) : "—"); - chargeTotalCost.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%.2f ₺", session.totalCost) : "—"); + chargeTotalCost.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "€%.2f", session.totalCost) : "—"); chargeGraph.setPoints(session.points); chargeCurveTable.setText(chargeGraph.tableText()); } diff --git a/app/src/main/res/layout/pane_charging.xml b/app/src/main/res/layout/pane_charging.xml index ce2c3f0..2a3f810 100644 --- a/app/src/main/res/layout/pane_charging.xml +++ b/app/src/main/res/layout/pane_charging.xml @@ -17,7 +17,7 @@ - + diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index c549b56..1acf22d 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -9,4 +9,4 @@ resources: - ingress.yaml images: - name: git.stinnesbeck.com/nils/mg4-server - newTag: "1.1.1" + newTag: "1.1.2" diff --git a/web/package-lock.json b/web/package-lock.json index 6f9ca13..88db69d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "nilsmg4-web", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nilsmg4-web", - "version": "1.1.1", + "version": "1.1.2", "dependencies": { "@influxdata/influxdb-client": "^1.35.0", "next": "^15.5.4", diff --git a/web/package.json b/web/package.json index 0c9abcf..c1a59a3 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "nilsmg4-web", - "version": "1.1.1", + "version": "1.1.2", "private": true, "scripts": { "dev": "next dev", diff --git a/web/src/app/charging/page.tsx b/web/src/app/charging/page.tsx index 16f8279..6cf9e42 100644 --- a/web/src/app/charging/page.tsx +++ b/web/src/app/charging/page.tsx @@ -12,10 +12,10 @@ export default async function ChargingPage() { if (!installationId || sessions.length === 0) { return ( - +

- Şarj kaydı yok. Yalnızca 10 kW üstü DC şarjlar ölçülür; AC şarj kaydedilmez. + No charging sessions yet. Only DC charges above 10 kW are metered; AC charging is not recorded.

@@ -25,22 +25,22 @@ export default async function ChargingPage() { const total = sumCharging(sessions); return ( - +
- - - + + + 0 ? money(total.cost / total.gridEnergyKwh) : "—"} - note="kWh başına" + note="per kWh" tone="teal" /> - +
-

Seanslar

+

Sessions

    {sessions.map((session) => { const gained = @@ -59,7 +59,7 @@ export default async function ChargingPage() {
    {session.isDemo && "Demo · "} {duration(session.durationSeconds)} - {gained !== null && ` · %${num(session.startSocPercent as number)} → %${num(session.endSocPercent as number)}`} + {gained !== null && ` · ${num(session.startSocPercent as number)}% → ${num(session.endSocPercent as number)}%`}
    @@ -77,16 +77,16 @@ export default async function ChargingPage() { }))} />
    - - - - - - - 0 ? `${num(peak)} kW` : "—"} /> - 0 ? `${num(average)} kW` : "—"} /> - - + + + + + + + 0 ? `${num(peak)} kW` : "—"} /> + 0 ? `${num(average)} kW` : "—"} /> + +
    diff --git a/web/src/app/consumption/page.tsx b/web/src/app/consumption/page.tsx index 163546a..d87b07a 100644 --- a/web/src/app/consumption/page.tsx +++ b/web/src/app/consumption/page.tsx @@ -18,9 +18,9 @@ export default async function ConsumptionPage({ if (!installationId || days.length === 0) { return ( - +
    -

    Günlük tüketim kaydı yok. Araç günü kapattığında yüklenir.

    +

    No daily consumption records yet. The car uploads them as it drives.

    ); @@ -46,14 +46,14 @@ export default async function ConsumptionPage({ return ( {/* Date filter */}
    - + {hasFilter && ( - Temizle + Clear )}
    @@ -80,60 +80,60 @@ export default async function ConsumptionPage({ {validFilter && selected ? ( <>
    - - - + + + 0 ? num(selected.day.km / selected.day.hours) : "—"} unit={selected.day.hours > 0 ? "km/h" : undefined} />
    - - - + + +
    ) : ( -

    Bu tarih için tüketim kaydı yok.

    +

    No consumption recorded for this date.

    )}
    )}
    - - - + + + 0 ? num(total.km / total.hours) : "—"} unit={total.hours > 0 ? "km/h" : undefined} />
    -

    Günlük enerji

    +

    Daily energy

    -

    Verimlilik eğilimi

    +

    Efficiency trend

    -

    Ömür boyu

    +

    Lifetime

    - - - - + + + +
    -

    Gün gün

    +

    Day by day

      {table.map((day) => { const isSelected = hasFilter && day.date === selectedDate; @@ -158,13 +158,13 @@ export default async function ConsumptionPage({
      - + 0 ? `${num(day.day.km / day.day.hours)} km/h` : "—"} /> - - + +
      diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index a39b78a..d4242ec 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -3,7 +3,7 @@ import "./globals.css"; export const metadata: Metadata = { title: "Nils' MG4", - description: "MG4 tüketim ve şarj geçmişi", + description: "MG4 consumption and charging history", }; export const viewport: Viewport = { @@ -16,7 +16,7 @@ export const viewport: Viewport = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} ); diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 8c67603..88cabd7 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -14,7 +14,7 @@ export default async function LoginPage({

      Nils' MG4

      -

      Tüketim ve şarj geçmişi

      +

      Consumption and charging history

      {/* A hidden username so password managers will offer to save this. Most of them ignore a lone password field — they key a saved entry on @@ -33,15 +33,15 @@ export default async function LoginPage({ className="field" type="password" name="password" - placeholder="Parola" + placeholder="Password" autoComplete="current-password" autoFocus required /> - {error &&

      Parola hatalı.

      } + {error &&

      Wrong password.

      }
      ); diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index a6cae0f..6b1dc26 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -12,10 +12,10 @@ export default async function OverviewPage() { if (!installationId) { return ( - +

      - Henüz veri yok. Araç ilk günlük kaydı gönderdiğinde burada görünecek. + No data yet. It appears here once the car sends its first daily record.

      @@ -31,26 +31,26 @@ export default async function OverviewPage() { return ( 1 ? `${installationCount} araçtan en günceli` : "MG4"} + title="Overview" + subtitle={installationCount > 1 ? `Most recent of ${installationCount} cars` : "MG4"} >
      0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh ort.` : undefined} + note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh avg.` : undefined} tone="gold" /> - +
      -

      Son 30 gün

      +

      Last 30 days

      ({ date: day.date, kwh: day.day.kwh, km: day.day.km }))} />
      {lastSession && (
      -

      Son şarj

      +

      Last charge

      - - - + + + - - + +
      )}
      -

      Batarya ve toplamlar

      +

      Battery and totals

      - - - 0 ? `${num(charge.peakKw)} kW` : "—"} /> - 0 ? `%${num(charge.socGained)}` : "—"} /> - + + + 0 ? `${num(charge.peakKw)} kW` : "—"} /> + 0 ? `${num(charge.socGained)}%` : "—"} /> +
      diff --git a/web/src/components/charts.tsx b/web/src/components/charts.tsx index b02c6be..351f5f6 100644 --- a/web/src/components/charts.tsx +++ b/web/src/components/charts.tsx @@ -78,7 +78,7 @@ export interface DayDatum { * distinction the telemetry side makes between a failed read and a zero. */ export function DailyEnergyChart({ days }: { days: DayDatum[] }) { - if (days.length === 0) return

      Henüz günlük kayıt yok

      ; + if (days.length === 0) return

      No daily records yet

      ; const plot = PLOT; const max = niceMax(Math.max(...days.map((day) => day.kwh))); const slot = innerWidth(plot) / days.length; @@ -86,7 +86,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) { return ( <> + aria-label="Energy used per day"> value.toFixed(0)} /> {days.map((day, index) => { const height = (day.kwh / max) * innerHeight(plot); @@ -116,7 +116,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
      - Günlük enerji (kWh) + Daily energy (kWh)
      @@ -128,7 +128,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) { const points = days .filter((day) => day.km >= 1 && day.kwh > 0) .map((day) => ({ date: day.date, value: (day.kwh * 100) / day.km })); - if (points.length < 2) return

      Grafik için yeterli gün yok

      ; + if (points.length < 2) return

      Not enough days for a chart

      ; const plot = PLOT; const max = niceMax(Math.max(...points.map((point) => point.value))); const step = innerWidth(plot) / Math.max(1, points.length - 1); @@ -144,7 +144,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) { return ( <> + aria-label="Average consumption per day"> value.toFixed(0)} /> - Ortalama {average.toFixed(1)} + Average {average.toFixed(1)}
      @@ -196,7 +196,7 @@ export interface CurveDatum { * them — the taper is only legible next to the percentage that caused it. */ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) { - if (curve.length < 2) return

      Bu seans için eğri kaydı yok

      ; + if (curve.length < 2) return

      No curve recorded for this session

      ; const plot = { ...PLOT, padRight: 26 }; const times = curve.map((point) => new Date(point.timestamp).getTime()); const start = times[0]; @@ -223,7 +223,7 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) { return ( <> + aria-label="Charging power and state-of-charge curve"> value.toFixed(0)} /> @@ -241,20 +241,20 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) { 0% - 0 dk + 0 min - {Math.round(span / 60000)} dk + {Math.round(span / 60000)} min
      - Güç (kW) + Power (kW) - Doluluk (%) + State of charge (%)
      diff --git a/web/src/components/shell.tsx b/web/src/components/shell.tsx index a9320aa..242adc8 100644 --- a/web/src/components/shell.tsx +++ b/web/src/components/shell.tsx @@ -2,9 +2,9 @@ import Link from "next/link"; import type { ReactNode } from "react"; const TABS = [ - { href: "/", label: "Özet", icon: "◈" }, - { href: "/consumption", label: "Tüketim", icon: "▤" }, - { href: "/charging", label: "Şarj", icon: "⚡" }, + { href: "/", label: "Overview", icon: "◈" }, + { href: "/consumption", label: "Consumption", icon: "▤" }, + { href: "/charging", label: "Charging", icon: "⚡" }, ] as const; export type Tab = (typeof TABS)[number]["href"]; @@ -42,7 +42,7 @@ export function Shell({ */}
      diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts index 19869fc..4343772 100644 --- a/web/src/lib/format.ts +++ b/web/src/lib/format.ts @@ -1,10 +1,10 @@ -/** Turkish formatting for every user-facing number and date. */ +/** English formatting (en-GB: 24-hour clock, day-month dates) with euro for every number and date. */ -const NUMBER = new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 1 }); -const PRECISE = new Intl.NumberFormat("tr-TR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -const CURRENCY = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY", maximumFractionDigits: 2 }); -const DATE = new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "long", year: "numeric" }); -const DATE_TIME = new Intl.DateTimeFormat("tr-TR", { +const NUMBER = new Intl.NumberFormat("en-GB", { maximumFractionDigits: 1 }); +const PRECISE = new Intl.NumberFormat("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +const CURRENCY = new Intl.NumberFormat("en-GB", { style: "currency", currency: "EUR", maximumFractionDigits: 2 }); +const DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric" }); +const DATE_TIME = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", @@ -32,13 +32,13 @@ export function dateTime(value: Date | string): string { return DATE_TIME.format(new Date(value)); } -/** `2 sa 05 dk` — the same shape the car's charging pane uses. */ +/** `2 h 05 min` — the same shape the car's charging pane uses. */ export function duration(seconds: number): string { const total = Math.max(0, Math.round(seconds)); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); - if (hours === 0) return `${minutes} dk`; - return `${hours} sa ${String(minutes).padStart(2, "0")} dk`; + if (hours === 0) return `${minutes} min`; + return `${hours} h ${String(minutes).padStart(2, "0")} min`; } /** Energy per 100 km, or a dash — never a ratio computed from a distance of nothing. */