diff --git a/Services/LayoutStore.Assets.cs b/Services/LayoutStore.Assets.cs
index 11e66ec..7710aec 100644
--- a/Services/LayoutStore.Assets.cs
+++ b/Services/LayoutStore.Assets.cs
@@ -35,6 +35,20 @@ public partial class LayoutStore : IDisposable
return (string)cmd.ExecuteScalar()!;
}
+ /// True when the asset already holds exactly these bytes. Used to decide whether a
+ /// bundled image needs re-seeding: comparing bytes cannot go stale the way a version constant
+ /// can (the constant and the embedded resource were built at different moments twice).
+ public bool AssetMatches(string assetId, byte[] data)
+ {
+ if (string.IsNullOrWhiteSpace(assetId)) return false;
+ var hash = Convert.ToHexString(SHA256.HashData(data));
+ using var cmd = _connection.CreateCommand();
+ cmd.CommandText = "SELECT 1 FROM Asset WHERE Id = $id AND Hash = $hash;";
+ cmd.Parameters.AddWithValue("$id", assetId);
+ cmd.Parameters.AddWithValue("$hash", hash);
+ return cmd.ExecuteScalar() != null;
+ }
+
// ─── License (TASK 10 — Polar billing) ───
/// The saved license key, or null if never entered.
diff --git a/Services/LayoutStore.Settings.cs b/Services/LayoutStore.Settings.cs
index 2ff1d56..fcdaf09 100644
--- a/Services/LayoutStore.Settings.cs
+++ b/Services/LayoutStore.Settings.cs
@@ -63,12 +63,6 @@ public partial class LayoutStore : IDisposable
public void SaveShowMonetaryMetrics(bool show) => UpsertSetting("Metrics.ShowMonetary", show ? "1" : "0");
- /// Which vintage of the shipped default scene art this layout was stamped with. Bumped when
- /// a bundled background is replaced, so existing layouts re-seed instead of keeping the old art.
- public int LoadDefaultArtVersion() => int.TryParse(GetSetting("Art.DefaultVersion"), out var v) ? v : 1;
-
- public void SaveDefaultArtVersion(int version) => UpsertSetting("Art.DefaultVersion", version.ToString());
-
public bool LoadAutoChromaScreen() => GetSetting("Video.AutoChromaScreen") == "1";
public void SaveAutoChromaScreen(bool enabled) => UpsertSetting("Video.AutoChromaScreen", enabled ? "1" : "0");
diff --git a/ViewModels/MainViewModel.Background.cs b/ViewModels/MainViewModel.Background.cs
index 97b12f6..5d50c70 100644
--- a/ViewModels/MainViewModel.Background.cs
+++ b/ViewModels/MainViewModel.Background.cs
@@ -427,37 +427,9 @@ public partial class MainViewModel
/// Heals every scene to the locked Background model after a layout
/// load: keeps the correct flavor per scene at index 0 named "Background",
/// drops duplicates/stale rows, re-seeds missing ones.
- /// Bump when a bundled background image is replaced. Layouts stamped with an older version
- /// re-seed their DEFAULT art on next load; a creator's own custom background is never touched.
- private const int CurrentDefaultArtVersion = 4;
-
- /// Re-stamp the shipped art for scenes still using the default. Without this, a replaced
- /// starting-background.jpg reaches new installs only: the asset is hash-keyed into the layout
- /// database on first seed, so an existing profile keeps showing the old image forever.
- private void HealDefaultArtVersion()
- {
- if (_layoutStore.LoadDefaultArtVersion() >= CurrentDefaultArtVersion) return;
-
- // Every scene's background, not AllBackgrounds(): that helper only returns the LIVE scene's
- // capture-flavoured rows, which is why version 2 healed nothing (2026-11-06).
- var cleared = 0;
- foreach (var scene in Scenes)
- foreach (var background in scene.Elements.OfType().Where(element => element.IsBackground))
- {
- if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath))
- continue; // the creator chose their own art
- background.AssetId = null; // the seed below re-stamps the bundled art
- cleared++;
- }
-
- AppLog.Write($"Default art version {CurrentDefaultArtVersion}: re-seeding {cleared} background(s)");
- _layoutStore.SaveDefaultArtVersion(CurrentDefaultArtVersion);
- }
-
private void HealBackgrounds()
{
NormalizeBackgrounds(Scenes);
- HealDefaultArtVersion();
foreach (var scene in Scenes)
{
_sceneGraph.InvalidateBake(scene);
@@ -516,7 +488,6 @@ public partial class MainViewModel
/// carries none. A custom image (Browse…) always wins and is never overwritten.
private void StampDefaultBackgroundAsset(Source background, string sceneName)
{
- if (!string.IsNullOrWhiteSpace(background.AssetId)) return;
if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath)) return;
var asset = DefaultBackgroundAsset(sceneName);
if (asset == null) return;
@@ -531,7 +502,13 @@ public partial class MainViewModel
if (stream == null) return;
using var ms = new MemoryStream();
stream.CopyTo(ms);
- background.AssetId = AddAsset(ms.ToArray());
+ var bytes = ms.ToArray();
+
+ // Already the shipped art → leave it. Compared by BYTES, not by a version stamp: assets are
+ // hash-keyed on first seed, so a replaced bundled image never reaches an existing profile on
+ // its own, and the version constant proved just as stale (2026-11-06).
+ if (_layoutStore.AssetMatches(background.AssetId!, bytes)) return;
+ background.AssetId = AddAsset(bytes);
}
catch { /* non-critical — the app works without default art */ }
}
diff --git a/ytLive.Tests/BackgroundDefaultArtTests.cs b/ytLive.Tests/BackgroundDefaultArtTests.cs
index e567b4d..f3c17da 100644
--- a/ytLive.Tests/BackgroundDefaultArtTests.cs
+++ b/ytLive.Tests/BackgroundDefaultArtTests.cs
@@ -10,9 +10,10 @@ namespace ytLive.Tests;
///
/// 2026-11-06: replacing a bundled background reached new installs only. Default art is hash-keyed into the
-/// layout database on first seed, so an existing profile kept the old image for ever — and the first
-/// attempt at a version heal silently did nothing because it iterated `AllBackgrounds()`, which returns
-/// only the LIVE scene's rows.
+/// layout database on first seed, so an existing profile kept the old image for ever. A version-stamp heal
+/// failed twice (first it iterated `AllBackgrounds()`, which returns only the LIVE scene's rows; then the
+/// constant and the embedded resource were built at different moments). The seed now compares BYTES, which
+/// is what this test pins: a profile already pointing at other art is re-stamped without any version bump.
///
[Collection("RealApp")]
public sealed class BackgroundDefaultArtTests
@@ -35,8 +36,8 @@ public sealed class BackgroundDefaultArtTests
Assert.False(string.IsNullOrWhiteSpace(firstId), "the bundled Starting art never seeded");
var firstBytes = AssetLength(tempDb, firstId!);
- // Simulate a profile stamped with the OLD art: point the Starting background at a planted
- // asset and mark the layout as an older art vintage. The heal must re-stamp the shipped art.
+ // Simulate a profile stamped with the OLD art. No version flag is touched — the seed has to
+ // notice the bytes are not the shipped ones by itself.
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
{
connection.Open();
@@ -49,8 +50,6 @@ public sealed class BackgroundDefaultArtTests
plant.ExecuteNonQuery();
}
SqliteConnection.ClearAllPools();
- using (var store = new LayoutStore(tempDb))
- store.SaveDefaultArtVersion(1);
var second = new MainViewModel();
var secondId = StartingBackgroundId(second);
@@ -64,6 +63,10 @@ public sealed class BackgroundDefaultArtTests
AppContext.BaseDirectory, "..", "..", "..", "..", "Assets", "starting-background.jpg")).Length,
AssetLength(tempDb, secondId!));
Assert.NotEqual(firstBytes, 0);
+
+ // Idempotent: a third launch finds the shipped bytes already in place and must not re-seed.
+ var third = new MainViewModel();
+ Assert.Equal(secondId, StartingBackgroundId(third));
}
finally
{
diff --git a/ytLive.Tests/SceneThumbnailTests.cs b/ytLive.Tests/SceneThumbnailTests.cs
index c8699e2..ed15ac0 100644
--- a/ytLive.Tests/SceneThumbnailTests.cs
+++ b/ytLive.Tests/SceneThumbnailTests.cs
@@ -263,6 +263,11 @@ public sealed class SceneThumbnailTests
var id = LayoutStore.Instance!.UpsertAsset(bytes, 320, 180);
Assert.NotNull(id);
bg!.AssetId = id;
+ // The creator's own picture, not the shipped art: a default-art background pointing at
+ // other bytes is re-seeded by StampDefaultBackgroundAsset (2026-11-06), and this fixture
+ // needs its planted art to stay put. Browse… always sets the path alongside the asset.
+ bg.UseDefaultBackground = false;
+ bg.CustomBackgroundPath = Path.Combine(Path.GetTempPath(), $"ytLive-missing-{i}.jpg");
backgroundIds[scene.Name] = id;
}
builder.SaveLayoutNow();