Skip to content
Draft
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
f4aff26
Implemented charge windows instead of just picking random low price s…
fredli74 Feb 6, 2026
f0cabe5
logic.ts trailing whitespace
fredli74 Feb 6, 2026
6ee0dd7
Update server/logic.ts
fredli74 Feb 6, 2026
d77163b
Reuse now instead of Date.now()
fredli74 Feb 6, 2026
62ca0a6
Incorrectly removed current timeslot from price_data SQL
fredli74 Feb 6, 2026
2f49ce8
Inline score calculation for remainders.
fredli74 Feb 6, 2026
56f2dbf
Track requested charge levels in status messages
fredli74 Feb 6, 2026
dfdbcbc
Refactor soft intents: type definition and status function
fredli74 Feb 6, 2026
289a128
Disable ESLint for v-html in alerts
fredli74 Feb 6, 2026
edee93d
Still do SoftIntents during manual charge, because of trip scheduling
fredli74 Feb 7, 2026
cd6e37c
Fix backoff logic to always run at least once if we have a targetMs <…
fredli74 Feb 7, 2026
a05c30e
Fix: Apply fill plan only if windows exist and time sufficient
fredli74 Feb 7, 2026
9970b35
Tesla-agent would set charge limit (wantedSoc) too low and risk the v…
fredli74 Feb 7, 2026
abaa689
Improve logging
fredli74 Feb 8, 2026
0ba6c3c
Refactor charge scheduling to use dynamic programming instead of spli…
fredli74 Feb 12, 2026
d6249df
Refactor vehicle logging to use dedicated logger
fredli74 Feb 12, 2026
eff4b93
Increase warmup penalty to 15 minutes and add scheduling window logging.
fredli74 Feb 13, 2026
e792fc0
Fix existingPrecon check
fredli74 Feb 13, 2026
2469fd9
Add error logging for vehicleWork failures
fredli74 Feb 16, 2026
8ec1159
Add exponential backoff for telemetry config on 403 errors
fredli74 Feb 17, 2026
e20c353
Allow over max price for charging start windows
fredli74 Feb 20, 2026
f283d3f
Fix spelling of 'Telemetry' in logs and strings
fredli74 Feb 20, 2026
303905c
Move dotenv config and use serverURL in proxy target
fredli74 Feb 20, 2026
b7c3b2e
Add module temperature min/max telemetry support
fredli74 Feb 20, 2026
66d84c1
Add split charge control for vehicle settings
fredli74 Feb 20, 2026
c2d4356
Fix split charge label and warmup penalty calculation
fredli74 Feb 20, 2026
7d4afca
Fix: We did not respect the autoHvac setting
fredli74 Feb 23, 2026
47b15e8
tesla: redact secrets in logs
fredli74 Mar 11, 2026
5640500
Fix splitCharge default and saving state handling.
fredli74 Mar 17, 2026
3cd204f
Fix soft intent scheduling by keeping per-intent deadlines
fredli74 Apr 3, 2026
6f93098
Keep active charge legs sticky near completion
fredli74 Apr 3, 2026
98dee8f
Audit Tesla schedules against live vehicle state
fredli74 Apr 3, 2026
55ab6e1
Await queued Tesla work on telemetry scope backoff
fredli74 Apr 3, 2026
6cb27e4
Log updateVehicle against verified vehicle UUID
fredli74 Apr 3, 2026
2bf6ad5
Compile error because of missing import
fredli74 Apr 3, 2026
2b65953
Scope vehicle location save cleanup to each request
fredli74 Apr 3, 2026
94a38f8
Refactor charge intent handling with price planning constants
fredli74 Apr 4, 2026
3184af2
Refactor scheduling logic for step-based optimization
fredli74 Apr 10, 2026
4936876
Fix splitCharge type and location key in templates
fredli74 Apr 10, 2026
8930668
Add planStartIndex to track charge plan start position
fredli74 Apr 21, 2026
1337dcb
Replace deepmerge with ticket-based save state tracking
fredli74 Apr 21, 2026
240bff8
Refactor gap penalty logic for interruptions
fredli74 Apr 21, 2026
654fd5d
Fix stepMs for splitCharge.Always case
fredli74 Apr 21, 2026
fcb1d3c
Add disallowGaps check to block gap-containing compositions.
fredli74 Apr 21, 2026
16ab507
Refactor scheduling logic to prioritize tariff fidelity and phase tra…
fredli74 Apr 22, 2026
3617fc9
Add detailed logging for scheduling steps and node management.
fredli74 Apr 22, 2026
2dd33b0
Extract location settings logic into helper method
fredli74 Apr 22, 2026
cac8348
Replace string-based schedule comparison with structural equality che…
fredli74 Apr 22, 2026
8ead037
Refactor charging plan to track warmup debt and energy cost.
fredli74 Apr 22, 2026
c3fddbb
Replace compareStopTimes with numeric comparison in sort
fredli74 Apr 22, 2026
c0f3761
Fix form validation check and add else block for saving state.
fredli74 Apr 22, 2026
26458af
Refactor log formatting into helper function
fredli74 Apr 22, 2026
c767ffe
Fix: Sort schedules by start time instead of stop time
fredli74 Apr 22, 2026
770145f
Sort intents by start time instead of stop time.
fredli74 Apr 22, 2026
ea2929b
Enhance schedule management and emergency wake-up logic
fredli74 Apr 22, 2026
c24027f
Update manual schedule departure threshold to 3km for clearer detection.
fredli74 Apr 22, 2026
44b1f35
Remove setSmartStatusFromIntent and integrate status into charge plan
fredli74 Apr 22, 2026
ae599de
Log VIN always, include UUID when available
fredli74 Apr 22, 2026
2c474af
Move Tesla scheduling methods to public utilities.
fredli74 Apr 22, 2026
c2b0854
Add Node.js tests for Tesla agent logic
fredli74 Apr 22, 2026
40e31e0
Add trusted HTML comments to v-html usages
fredli74 Apr 22, 2026
df20a04
Replace splitCharge string with enum in resolvers and schema
fredli74 Apr 22, 2026
562a1fe
Refactor charge schedule quantum and adjust blocker logic for tariff …
fredli74 Apr 22, 2026
79979ac
Fix emergency wake-up time parsing and log formatting
fredli74 Apr 23, 2026
f49c50e
Fix: Skip schedule reconciliation if live schedules are unknown
fredli74 Apr 23, 2026
7acb956
Add schedule sync issue indicators and backend support
fredli74 Apr 23, 2026
868024c
Simplify schedule sync issue handling by removing reason and streamli…
fredli74 Apr 23, 2026
060e327
Update sync status colors and drift message wording
fredli74 Apr 23, 2026
7c89cc2
Move icon class to tooltip activator
fredli74 Apr 23, 2026
c0de92a
Use issueKind instead of hardcoded 'drift
fredli74 Apr 24, 2026
0eb6648
Update schedule tooltip and add SOC limit checks
fredli74 Apr 24, 2026
8e21af4
Fix schedule sync classification by checking start time only.
fredli74 Apr 27, 2026
919cd96
Add teslaWantedSocLimit function and test
fredli74 Apr 28, 2026
02dc9b8
Convert functions to async/await and add yield helper
fredli74 May 1, 2026
d3ce524
Fix: Retry mapping when no vehicles and state is empty
fredli74 May 1, 2026
546683e
Handle incomplete service data by skipping and returning null.
fredli74 May 1, 2026
c54af20
Only add routine charge intent when charging is needed.
fredli74 May 3, 2026
dbafe90
Fix autoHvac condition to check for true instead of not false
fredli74 May 13, 2026
0efc770
Filter schedule audit to SmartCharge IDs only and widen coordinate ep…
fredli74 May 17, 2026
291bf84
Disable audit by moving dates to past.
fredli74 May 31, 2026
84b18bf
Added script for docker log rotation
fredli74 Jun 5, 2026
58ed664
Clarify SmartCharge-authoritative schedule updates
fredli74 Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run test:node
3 changes: 3 additions & 0 deletions app/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@
tile
prominent
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="error.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
</v-alert>
<v-alert v-model="warning.show" dismissible type="warning" tile>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="warning.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
Comment thread
fredli74 marked this conversation as resolved.
</v-alert>
Comment thread
fredli74 marked this conversation as resolved.
<v-alert
Expand All @@ -85,6 +87,7 @@
tile
@input="closedInfo"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="info.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
Comment thread
fredli74 marked this conversation as resolved.
Comment on lines 77 to 94
</v-alert>
</v-flex>
Expand Down
34 changes: 24 additions & 10 deletions app/src/components/edit-location.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import gql from "graphql-tag";

import EditVehicleLocationSettings from "@app/components/edit-vehicle-location-settings.vue";
import RemoveDialog from "@app/components/remove-dialog.vue";
import deepmerge from "deepmerge";
import equal from "fast-deep-equal";
import { GQLLocation, GQLPriceList } from "@shared/sc-schema.js";
import { UpdateLocationParams } from "@shared/sc-client.js";
Expand Down Expand Up @@ -129,17 +128,26 @@ export default class EditLocation extends Vue {

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const update: UpdateLocationParams = {
id: this.location.id,
};
Expand All @@ -153,12 +161,18 @@ export default class EditLocation extends Vue {
delete update.providerData;
}

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateLocation(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateLocation(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
78 changes: 62 additions & 16 deletions app/src/components/edit-vehicle-location-settings.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<template>
<v-form ref="form">
<v-row>
<v-col cols="12" sm="5" md="6" class="mt-2">
<v-col cols="12" sm="4" md="5" class="mt-2">
<v-list-item-title>{{ name }}</v-list-item-title>
<v-list-item-subtitle
class="font-light overline caption secondary--text text--lighten-2"
>
({{ settings.locationID }})
</v-list-item-subtitle>
</v-col>
<v-spacer />
<v-col cols="6" sm="3" md="3">
<v-col cols="6" sm="3" md="2">
<v-text-field
v-model="directLevel"
:rules="[directLevelRules]"
Expand All @@ -33,7 +32,25 @@
</template>
</v-text-field>
</v-col>
<v-col cols="6" sm="4" md="3">
<v-col cols="6" sm="3" md="2">
<v-select
v-model="splitCharge"
:items="splitChargeList"
label="Split charge window"
placeholder=" "
:loading="saving.splitCharge"
>
<template #append-outer>
<v-tooltip bottom max-width="18rem">
<template #activator="{ on }">
<v-icon v-on="on">mdi-help-circle-outline</v-icon>
</template>
Control if charging is allowed to split into multiple windows.
</v-tooltip>
</template>
</v-select>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-combobox
v-model="goal"
:items="goalCBList"
Expand Down Expand Up @@ -62,7 +79,6 @@
v-model="focus"
active-class="selected-charge"
color="primary"
label="hej"
mandatory
>
<v-btn small>Low Cost</v-btn>
Expand All @@ -77,9 +93,8 @@

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import deepmerge from "deepmerge";
import { GQLVehicle, GQLVehicleLocationSetting } from "@shared/sc-schema.js";
import { SmartChargeGoal } from "@shared/sc-types.js";
import { SmartChargeGoal, SplitCharge } from "@shared/sc-types.js";
import { UpdateVehicleParams } from "@shared/sc-client.js";

@Component({})
Expand All @@ -90,18 +105,25 @@ export default class EditVehicle extends Vue {

saving!: { [key: string]: boolean };
goalCBList!: { text: string; value: string }[];
splitChargeList!: { text: string; value: string }[];
data() {
return {
saving: {
directLevel: false,
goal: false,
splitCharge: false,
},
goalCBList: [
{ text: "Low cost", value: SmartChargeGoal.Low },
{ text: "Balanced", value: SmartChargeGoal.Balanced },
{ text: "Full charge", value: SmartChargeGoal.Full },
{ text: "Custom", value: "%" },
],
splitChargeList: [
{ text: "Never", value: SplitCharge.Never },
{ text: "Auto", value: SplitCharge.Auto },
{ text: "Always", value: SplitCharge.Always },
],
Comment on lines 96 to +126
};
}

Expand Down Expand Up @@ -153,19 +175,36 @@ export default class EditVehicle extends Vue {
this.save("goal");
}

get splitCharge(): string {
return this.settings.splitCharge || SplitCharge.Auto;
}
Comment on lines +178 to +180
set splitCharge(value: string) {
this.settings.splitCharge = value;
this.save("splitCharge");
}

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const goal = this.settings.goal as any;
const update: UpdateVehicleParams = {
id: this.vehicle.id,
Expand All @@ -174,16 +213,23 @@ export default class EditVehicle extends Vue {
locationID: this.settings.locationID,
directLevel: this.settings.directLevel,
goal: goal.value || goal,
splitCharge: this.settings.splitCharge || SplitCharge.Auto,
} as GQLVehicleLocationSetting,
Comment on lines 212 to 217
],
};

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateVehicle(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateVehicle(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
Comment on lines +190 to 229

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearSaving merge/cleanup logic can clear loading states for fields that weren’t part of the request that just completed. Because clearSaving is merged from this.saving and then reused across saves, a later request finishing can set other fields’ saving.* to false while an earlier/other in-flight update is still running (especially if the user edits again while the previous request is in flight). Consider tracking saving state per request (e.g., per-field counters/tokens) or only clearing the specific field(s) included in the update payload for that request instead of iterating over a merged cross-request map.

Copilot uses AI. Check for mistakes.
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
54 changes: 37 additions & 17 deletions app/src/components/edit-vehicle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

<EditVehicleLocationSettings
v-for="l in locationSettings()"
:key="l.settings.location"
:key="l.settings.locationID"
:name="l.name"
:settings="l.settings"
:vehicle="vehicle"
Expand Down Expand Up @@ -94,7 +94,6 @@
import { Component, Vue, Prop } from "vue-property-decorator";
import EditVehicleLocationSettings from "@app/components/edit-vehicle-location-settings.vue";
import RemoveDialog from "@app/components/remove-dialog.vue";
import deepmerge from "deepmerge";
import equal from "fast-deep-equal";
import {
GQLVehicle,
Expand Down Expand Up @@ -141,18 +140,24 @@ export default class EditVehicle extends Vue {
return true;
}

getOrCreateLocationSettings(locationID: string): GQLVehicleLocationSetting {
if (!this.vehicle.locationSettings) {
this.$set(this.vehicle, "locationSettings", []);
}
const existing = this.vehicle.locationSettings.find((f) => f.locationID === locationID);
if (existing) return existing;
const created = DefaultVehicleLocationSettings(locationID);
this.vehicle.locationSettings.push(created);
return created;
}

locationSettings(): any[] {
return (
(this.locations &&
this.locations
.filter((l) => l.ownerID === this.vehicle.ownerID)
.map((l) => {
const settings: GQLVehicleLocationSetting =
(this.vehicle.locationSettings &&
this.vehicle.locationSettings.find(
(f) => f.locationID === l.id
)) ||
DefaultVehicleLocationSettings(l.id);
const settings = this.getOrCreateLocationSettings(l.id);
return {
name: l.name,
settings,
Expand Down Expand Up @@ -216,17 +221,26 @@ export default class EditVehicle extends Vue {

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const update: UpdateVehicleParams = {
id: this.vehicle.id,
providerData: {},
Expand All @@ -251,12 +265,18 @@ export default class EditVehicle extends Vue {
delete update.providerData;
}

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateVehicle(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateVehicle(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"_create:schema": "node dist/server/gql/build-schema-files.js",
"build:app": "vite build",
"build:server": "npm run _babel:shared && npm run _babel:providers && npm run _babel:server && npm run _create:schema",
"test:node": "node --test providers/**/*.test.mjs",
"test": "npm run build:server && npm run test:node",
"start:server": "node dist/server/server.js",
"start:worker": "node dist/server/agency.js -d INTERNAL_SERVICE_TOKEN http://localhost:3030"
},
Expand Down
Loading
Loading