From 75a2f8b17e7da2f6b42a3a1dbf829bc4933afab7 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 14:59:31 +0200 Subject: [PATCH 01/12] fix(ui): render VAG vehicle brand configuration --- web/settings/tabs/devices.js | 58 +++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index d08ca6ba8..699478dcc 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1186,24 +1186,48 @@ var isCloudDriver = !isVehicleDriver && !isApiCredsDriver && cap.http != null && !hasHostField && (hasAuthField || Object.keys(dcfg).length === 0); if (isVehicleDriver) { - // TeslaBLEProxy-style drivers only need the LAN IP of the - // proxy and the VIN it's paired to. "Verify connection" - // makes the backend issue a one-shot vehicle_data poll so - // the operator can confirm pairing before saving. var vcfg = d.config || {}; - html += '
Vehicle' + - '
' + - '' + - '' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
'; + var isVAGVehicle = (d.lua || '').indexOf('vag_vehicle.lua') >= 0; + if (isVAGVehicle) { + // VAG EU Data Act is a cloud vehicle driver, not a + // TeslaBLEProxy-style LAN driver. Brand is required by + // vag_vehicle.lua and must be part of the row so the generic + // connection probe receives it together with VIN and secrets. + var vagBrand = String(vcfg.brand || '').toLowerCase(); + html += '
Vehicle' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '
' + + '
'; + } else { + // TeslaBLEProxy-style drivers only need the LAN IP of the + // proxy and the VIN it's paired to. "Verify connection" + // makes the backend issue a one-shot vehicle_data poll so + // the operator can confirm pairing before saving. + html += '
Vehicle' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + } } if (isLocalHTTP) { var isZap = (d.lua || '').indexOf('zap.lua') >= 0; From 562d1aa5dc14d2190fb08b73599d6dcc887d0d07 Mon Sep 17 00:00:00 2001 From: Segran Date: Sat, 3 Oct 2026 16:08:22 +0200 Subject: [PATCH 02/12] fix(ci): update UI smoke test for overview view --- scripts/ci-ui-browser.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci-ui-browser.sh b/scripts/ci-ui-browser.sh index 7d2caf9e9..9de535d94 100755 --- a/scripts/ci-ui-browser.sh +++ b/scripts/ci-ui-browser.sh @@ -188,8 +188,8 @@ curl_json /api/status curl_json /api/drivers curl_json /api/config -smoke_page "$browser" / "view-live" "1440,1000" desktop +smoke_page "$browser" / "view-overview" "1440,1000" desktop smoke_page "$browser" /setup "wizard" "1440,1000" desktop -smoke_page "$browser" / "view-live" "390,844" mobile +smoke_page "$browser" / "view-overview" "390,844" mobile log "ok" From 474ebdc53e20b194e3a8a75ddae19df96913a0a8 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 19:17:13 +0200 Subject: [PATCH 03/12] fix(ui): configure VAG automatic sign-in --- web/settings/tabs/devices.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index 699478dcc..5de5c4496 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1194,7 +1194,12 @@ // vag_vehicle.lua and must be part of the row so the generic // connection probe receives it together with VIN and secrets. var vagBrand = String(vcfg.brand || '').toLowerCase(); - html += '
Vehicle' + + var vagHasPassword = d.has_password === true || + (typeof vcfg.password === 'string' && vcfg.password !== ''); + var vagPasswordBadge = vagHasPassword + ? '✓ Saved' + : '⚠ Not saved'; + html += '
VAG EU Data Act' + '
' + '' + '' + '
' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '
' + + '

' + + 'VAG driver v0.2.0+ signs in again automatically when the portal session expires. A pasted Cookie is only a fallback for older Core/driver versions.' + + '

' + '
'; } else { // TeslaBLEProxy-style drivers only need the LAN IP of the From f1d42d21fb3f683594ffef1eb178813a0bb5f00d Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 19:39:28 +0200 Subject: [PATCH 04/12] fix(ui): hydrate VAG HTTP allowlist from catalog --- web/settings/tabs/devices.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index 5de5c4496..d6b258d1f 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1189,6 +1189,18 @@ var vcfg = d.config || {}; var isVAGVehicle = (d.lua || '').indexOf('vag_vehicle.lua') >= 0; if (isVAGVehicle) { + // Existing VAG configs can predate catalog http_hosts and therefore + // carry capabilities.http.allowed_hosts=[] (or no allowlist at all). + // host.http_request deliberately refuses that, so hydrate the + // driver's signed catalog allowlist before Save/Test connection. + // Never derive these cloud hosts from operator input. + var vagHTTPHosts = (catalogEntry && catalogEntry.http_hosts) || []; + d.capabilities = d.capabilities || {}; + d.capabilities.http = d.capabilities.http || {}; + if (vagHTTPHosts.length > 0) { + d.capabilities.http.allowed_hosts = vagHTTPHosts.slice(); + } + // VAG EU Data Act is a cloud vehicle driver, not a // TeslaBLEProxy-style LAN driver. Brand is required by // vag_vehicle.lua and must be part of the row so the generic From 63b8bf0425835b567cc44635dd0af02f2d8c78f9 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 19:55:36 +0200 Subject: [PATCH 05/12] fix(drivers): hydrate HTTP allowlist from driver metadata --- go/internal/drivers/registry.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/go/internal/drivers/registry.go b/go/internal/drivers/registry.go index ca1f5a9e7..890ea14e0 100644 --- a/go/internal/drivers/registry.go +++ b/go/internal/drivers/registry.go @@ -590,6 +590,14 @@ func (r *Registry) add(ctx context.Context, cfg config.Driver, startupDefault bo if cfg.Capabilities.HTTP != nil { env.WithHTTP() hosts := mergeAllowedHosts(cfg.Capabilities.HTTP.AllowedHosts, cfg.Config) + // Cloud drivers declare their fixed network boundary in DRIVER.http_hosts. + // Older saved configs (and connection probes built from them) may predate + // that metadata and therefore have no capabilities.http.allowed_hosts. + // Hydrate only from the Lua driver's own declaration; never from operator + // input. Explicit config hosts are retained and merged above. + if entry, err := ParseCatalogFile(cfg.Lua); err == nil { + hosts = mergeAllowedHosts(hosts, map[string]any{"host": entry.HTTPHosts}) + } if len(hosts) > 0 { env.WithHTTPAllowedHosts(hosts) } From cd810271846ebb4e8b2a0d6493aa001b3d642c04 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 19:55:43 +0200 Subject: [PATCH 06/12] fix(drivers): merge declared HTTP hosts correctly --- go/internal/drivers/registry.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/go/internal/drivers/registry.go b/go/internal/drivers/registry.go index 890ea14e0..5a7e2bdcd 100644 --- a/go/internal/drivers/registry.go +++ b/go/internal/drivers/registry.go @@ -596,7 +596,12 @@ func (r *Registry) add(ctx context.Context, cfg config.Driver, startupDefault bo // Hydrate only from the Lua driver's own declaration; never from operator // input. Explicit config hosts are retained and merged above. if entry, err := ParseCatalogFile(cfg.Lua); err == nil { - hosts = mergeAllowedHosts(hosts, map[string]any{"host": entry.HTTPHosts}) + for _, h := range entry.HTTPHosts { + h = strings.TrimSpace(h) + if h != "" && !slices.Contains(hosts, h) { + hosts = append(hosts, h) + } + } } if len(hosts) > 0 { env.WithHTTPAllowedHosts(hosts) From 6de237e8b49e827b3b42d93e2546a0d59a019205 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 20:06:45 +0200 Subject: [PATCH 07/12] fix(ui): identify managed VAG vehicle drivers --- web/settings/tabs/devices.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index d6b258d1f..491a591cd 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1187,7 +1187,12 @@ (hasAuthField || Object.keys(dcfg).length === 0); if (isVehicleDriver) { var vcfg = d.config || {}; - var isVAGVehicle = (d.lua || '').indexOf('vag_vehicle.lua') >= 0; + // Match both the configured logical path and the catalog entry. + // Repository-installed drivers may use a versioned/managed path, so + // filename-only detection can misclassify VAG as TeslaBLEProxy and + // render Proxy IP while hiding the VAG email fieldset. + var isVAGVehicle = (d.lua || '').indexOf('vag_vehicle.lua') >= 0 || + !!(catalogEntry && catalogEntry.id === 'vag_vehicle'); if (isVAGVehicle) { // Existing VAG configs can predate catalog http_hosts and therefore // carry capabilities.http.allowed_hosts=[] (or no allowlist at all). From 0ecb55dbe83988cb57db55e212e6b5e7d299be3b Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 20:13:08 +0200 Subject: [PATCH 08/12] fix(ui): hide legacy VAG cookie secret --- web/settings/tabs/devices.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index 491a591cd..5938a2502 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1794,6 +1794,16 @@ return k !== 'client_secret' && k !== 'refresh_token'; }); } + // VAG v0.2.0+ renders email/password in its dedicated fieldset. + // Cookie is retained in config as a legacy fallback for older + // Core/driver versions, but it is not part of the normal UI. + // Do not delete or overwrite an already saved cookie here. + var isVAG = (d.lua || '').indexOf('vag_vehicle.lua') >= 0 || + !!(entry && entry.id === 'vag_vehicle'); + if (isVAG) { + secrets = secrets.filter(function (k) { return k !== 'cookie'; }); + } + // Cloud credentials already render config.password. A second // Secrets field bound to the same path (Easee, Zaptec) saves // whichever input is read last and shows the wrong hint. From 6d10a76bef73df1fddafa5e5766816969465fd61 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 20:14:54 +0200 Subject: [PATCH 09/12] test(drivers): cover declared HTTP host hydration --- .../drivers/registry_allowlist_test.go | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/go/internal/drivers/registry_allowlist_test.go b/go/internal/drivers/registry_allowlist_test.go index 2fe07658b..79a18aaf4 100644 --- a/go/internal/drivers/registry_allowlist_test.go +++ b/go/internal/drivers/registry_allowlist_test.go @@ -1,6 +1,9 @@ package drivers import ( + "context" + "os" + "path/filepath" "reflect" "testing" @@ -156,3 +159,79 @@ func TestTcpAllowedHostsFor(t *testing.T) { }) } } + + +// A cloud driver owns its fixed network boundary in DRIVER.http_hosts. +// Existing configs may predate that metadata and therefore carry an empty +// capabilities.http.allowed_hosts. Both ordinary startup and connection +// probes must hydrate the same driver-declared hosts before Lua starts. +func TestRegistryHydratesHTTPHostsFromDriverMetadata(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "cloud.lua") + src := `DRIVER = { + id = "cloud", + name = "Cloud", + read_only = true, + protocols = { "http" }, + capabilities = { "vehicle" }, + http_hosts = { "api.example.test", " identity.example.test ", "api.example.test" }, +} +function driver_init(config) + local r, err = host.http_request{url = "https://not-declared.invalid/data"} + assert(r == nil, "unexpected request") + assert(err and err:find("not in allowed_hosts", 1, true), + "driver-declared allowlist was not installed: " .. tostring(err)) +end +function driver_poll() return 60000 end +` + if err := os.WriteFile(path, []byte(src), 0600); err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + add func(*Registry, context.Context, config.Driver) error + stop func(*Registry, string) + }{ + { + name: "startup", + add: func(r *Registry, ctx context.Context, cfg config.Driver) error { + return r.Add(ctx, cfg) + }, + stop: func(r *Registry, name string) { r.Remove(name) }, + }, + { + name: "probe", + add: func(r *Registry, ctx context.Context, cfg config.Driver) error { + return r.AddProbe(ctx, cfg) + }, + stop: func(r *Registry, name string) { r.RemoveProbe(name) }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + r := NewRegistry(nil) + cfg := config.Driver{ + Name: "cloud-" + tc.name, + Lua: path, + Capabilities: config.Capabilities{ + HTTP: &config.HTTPCapability{}, + }, + } + if err := tc.add(r, context.Background(), cfg); err != nil { + t.Fatalf("add with DRIVER.http_hosts: %v", err) + } + t.Cleanup(func() { tc.stop(r, cfg.Name) }) + + r.mu.Lock() + rd := r.rec[cfg.Name] + r.mu.Unlock() + if rd == nil { + t.Fatal("driver was not registered") + } + want := []string{"api.example.test", "identity.example.test"} + if !reflect.DeepEqual(rd.env.HTTPAllowedHosts, want) { + t.Fatalf("HTTPAllowedHosts = %v, want %v", rd.env.HTTPAllowedHosts, want) + } + }) + } +} From ace1178a7bb389afa47a483297c1b3361bbf5056 Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 20:15:00 +0200 Subject: [PATCH 10/12] test(drivers): use telemetry store in HTTP host test --- go/internal/drivers/registry_allowlist_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go/internal/drivers/registry_allowlist_test.go b/go/internal/drivers/registry_allowlist_test.go index 79a18aaf4..26edb628c 100644 --- a/go/internal/drivers/registry_allowlist_test.go +++ b/go/internal/drivers/registry_allowlist_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/telemetry" ) func TestMergeAllowedHosts(t *testing.T) { @@ -209,7 +210,7 @@ function driver_poll() return 60000 end }, } { t.Run(tc.name, func(t *testing.T) { - r := NewRegistry(nil) + r := NewRegistry(telemetry.NewStore()) cfg := config.Driver{ Name: "cloud-" + tc.name, Lua: path, From 541dcc161722a58b17bfcf8abe95141732c9338c Mon Sep 17 00:00:00 2001 From: segran2 Date: Sat, 3 Oct 2026 20:15:10 +0200 Subject: [PATCH 11/12] chore: add VAG cloud auth changeset --- .changeset/fix-vag-cloud-auth.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-vag-cloud-auth.md diff --git a/.changeset/fix-vag-cloud-auth.md b/.changeset/fix-vag-cloud-auth.md new file mode 100644 index 000000000..838e91f87 --- /dev/null +++ b/.changeset/fix-vag-cloud-auth.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Fix VAG EU Data Act setup and automatic sign-in by hydrating driver-declared HTTP hosts in Core, and show the VAG brand, VIN, email, and password fields without proxy or legacy cookie controls. From d1262779ed824329d453e08279d2ec765fb2d996 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 4 Oct 2026 07:31:51 +0200 Subject: [PATCH 12/12] fix(ui): leave the VAG HTTP allowlist to Core Core now merges DRIVER.http_hosts into the allowlist, so the settings page no longer replaces capabilities.http.allowed_hosts on render. That replacement dropped any host the operator had added. Also drop the hint about pasting a cookie, since the cookie field is hidden, and gofmt the test. Co-Authored-By: Claude Opus 5.5 --- go/internal/drivers/registry_allowlist_test.go | 1 - web/settings/tabs/devices.js | 15 +++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/go/internal/drivers/registry_allowlist_test.go b/go/internal/drivers/registry_allowlist_test.go index 26edb628c..1c7ae01b7 100644 --- a/go/internal/drivers/registry_allowlist_test.go +++ b/go/internal/drivers/registry_allowlist_test.go @@ -161,7 +161,6 @@ func TestTcpAllowedHostsFor(t *testing.T) { } } - // A cloud driver owns its fixed network boundary in DRIVER.http_hosts. // Existing configs may predate that metadata and therefore carry an empty // capabilities.http.allowed_hosts. Both ordinary startup and connection diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js index 5938a2502..01c32b9ac 100644 --- a/web/settings/tabs/devices.js +++ b/web/settings/tabs/devices.js @@ -1194,17 +1194,8 @@ var isVAGVehicle = (d.lua || '').indexOf('vag_vehicle.lua') >= 0 || !!(catalogEntry && catalogEntry.id === 'vag_vehicle'); if (isVAGVehicle) { - // Existing VAG configs can predate catalog http_hosts and therefore - // carry capabilities.http.allowed_hosts=[] (or no allowlist at all). - // host.http_request deliberately refuses that, so hydrate the - // driver's signed catalog allowlist before Save/Test connection. - // Never derive these cloud hosts from operator input. - var vagHTTPHosts = (catalogEntry && catalogEntry.http_hosts) || []; - d.capabilities = d.capabilities || {}; - d.capabilities.http = d.capabilities.http || {}; - if (vagHTTPHosts.length > 0) { - d.capabilities.http.allowed_hosts = vagHTTPHosts.slice(); - } + // Core merges the driver's DRIVER.http_hosts into the allowlist, + // so the UI leaves capabilities.http.allowed_hosts as saved. // VAG EU Data Act is a cloud vehicle driver, not a // TeslaBLEProxy-style LAN driver. Brand is required by @@ -1239,7 +1230,7 @@ (vagHasPassword ? '•••••••• (leave empty to keep)' : 'enter account password') + '">' + '' + '

' + - 'VAG driver v0.2.0+ signs in again automatically when the portal session expires. A pasted Cookie is only a fallback for older Core/driver versions.' + + 'VAG driver v0.2.0+ signs in again automatically when the portal session expires.' + '

' + '
'; } else {