${this._t("import.hint")}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index a4bde714..f58656e8 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+- Creating a space no longer reports `v8+ space requires wall_segments` while
+ leaving an unsaved ghost space on screen; a failed space save now restores
+ the server-backed state before another create or delete
+ ([#324](https://github.com/Matysh/houseplan-card/issues/324)).
+
## v1.68.0 — 2026-08-27
- Structural plan edits now rely on stable wall-segment IDs: thickness, draft
diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md
index 083072b7..6d482bc0 100755
--- a/docs/CHANGELOG.ru.md
+++ b/docs/CHANGELOG.ru.md
@@ -8,6 +8,12 @@
## Не выпущено
+- Создание пространства больше не выдаёт ошибку
+ `v8+ space requires wall_segments` и не оставляет на экране несохранённое
+ пространство-призрак; после неудачного сохранения состояние возвращается к
+ серверному до следующего создания или удаления
+ ([#324](https://github.com/Matysh/houseplan-card/issues/324)).
+
## v1.68.0 — 2026-08-27
- Структурные изменения плана теперь опираются на стабильные ID сегментов стен:
diff --git a/docs/images/screenshots.json b/docs/images/screenshots.json
index 397fe177..0f8759df 100644
--- a/docs/images/screenshots.json
+++ b/docs/images/screenshots.json
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
- "sourceFingerprint": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceFingerprint": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,7 +14,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
},
"view-touch": {
@@ -25,7 +25,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "41e3ba67f8db0e98f26f484293af83ef937c369ca5ca6a59a3350d8954c906f4"
},
"space-create": {
@@ -36,7 +36,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
},
"room-contour-close": {
@@ -47,7 +47,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
},
"plan-context-tray": {
@@ -58,7 +58,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
},
"device-editor": {
@@ -69,7 +69,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
},
"device-display-preview": {
@@ -80,7 +80,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
},
"background-editor": {
@@ -91,7 +91,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
},
"room-card": {
@@ -102,7 +102,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
@@ -113,7 +113,7 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "d3f9271c3a64836abe0b0907e6f33c8c78cca844bc1cdb5f96d198d664f457bb",
+ "sourceSha256": "bc59df31ef3ffbe595e905160b5d400b2317003d3f220d4a2232fa47068677cb",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
}
}
diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts
index 595fd857..0586e3d0 100755
--- a/src/houseplan-card.ts
+++ b/src/houseplan-card.ts
@@ -93,7 +93,7 @@ import {
} from './initial-load';
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
import {
- initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
+ createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
} from './space-dialog';
import { mdiHomeCityOutline } from '@mdi/js';
import {
@@ -14978,14 +14978,7 @@ class HouseplanCard extends LitElement {
const cfg = this._serverCfg!;
let sp: any;
if (d.mode === 'create') {
- sp = {
- id: spaceId,
- title: d.title.trim(),
- plan_url: null,
-
- view_box: [0, 0, 1, 1],
- rooms: [],
- };
+ sp = createEmptySpaceConfig(spaceId, d.title.trim());
cfg.spaces.push(sp);
} else {
sp = cfg.spaces.find((x: any) => x.id === spaceId);
@@ -15085,6 +15078,14 @@ class HouseplanCard extends LitElement {
}
}
} catch (e: any) {
+ // The dialog edits the reactive config optimistically before config/set.
+ // A rejected create used to leave that unsaved space on screen; the next
+ // delete then flushed the same malformed write and repeated its error.
+ // Re-adopt server truth for every failed space transaction. Conflict is
+ // already reloaded by _saveConfigNow; do not issue the same read twice.
+ // A lost response is safe too: if the server did commit, config/get
+ // returns it.
+ if (e?.code !== 'conflict') await this._reloadConfigOnly(true);
// audit L3: the dialog may have been closed (Esc) while the save was
// in flight — spreading null yields a truthy husk and the renderer
// then crashes, blanking the whole card. The toast below is the
diff --git a/src/space-dialog.ts b/src/space-dialog.ts
index 1fbebdb5..6d8b3f20 100644
--- a/src/space-dialog.ts
+++ b/src/space-dialog.ts
@@ -8,6 +8,23 @@ export interface SpaceDisplayDraft {
displayTouched: boolean;
}
+/**
+ * A current empty space is still a complete wall-model document. In
+ * particular, model v8+ forbids using an absent catalogue to mean "no walls":
+ * absence means an outdated/partial writer, while an empty array is the
+ * canonical empty catalogue.
+ */
+export function createEmptySpaceConfig(id: string, title: string): Record
{
+ return {
+ id,
+ title,
+ plan_url: null,
+ view_box: [0, 0, 1, 1],
+ rooms: [],
+ wall_segments: [],
+ };
+}
+
/** Source-specific visible defaults for a fresh create/onboarding step. */
export function initialSpaceDisplayDraft(source: SpacePlanSource = 'file'): SpaceDisplayDraft {
const visible = source === 'draw';
diff --git a/test/space-dialog.test.mjs b/test/space-dialog.test.mjs
index 4d0ebb04..3341e1c6 100644
--- a/test/space-dialog.test.mjs
+++ b/test/space-dialog.test.mjs
@@ -2,11 +2,23 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {
+ createEmptySpaceConfig,
initialSpaceDisplayDraft,
switchSpacePlanSource,
touchSpaceDisplay,
} from '../test-build/space-dialog.js';
+test('a fresh space is a complete empty v8/v9 wall-model document (#324)', () => {
+ assert.deepEqual(createEmptySpaceConfig('floor', 'Ground floor'), {
+ id: 'floor',
+ title: 'Ground floor',
+ plan_url: null,
+ view_box: [0, 0, 1, 1],
+ rooms: [],
+ wall_segments: [],
+ });
+});
+
test('fresh create projects honest File and Draw display defaults', () => {
const file = initialSpaceDisplayDraft();
assert.deepEqual(file, {
diff --git a/tests_backend/test_validation.py b/tests_backend/test_validation.py
index ab2e5e6f..1e7f440f 100644
--- a/tests_backend/test_validation.py
+++ b/tests_backend/test_validation.py
@@ -1718,6 +1718,12 @@ def test_zero_wall_style_and_v9_legacy_fields_are_strict():
"spaces": [{**base, "wall_segments": []}],
}
assert v.CONFIG_SCHEMA(canonical) == canonical
+ with pytest.raises(vol.Invalid, match=r"v8\+ space requires wall_segments"):
+ v.CONFIG_SCHEMA({
+ **canonical,
+ "spaces": [{key: value for key, value in canonical["spaces"][0].items()
+ if key != "wall_segments"}],
+ })
with pytest.raises(vol.Invalid, match="legacy open boundaries"):
v.CONFIG_SCHEMA({
**canonical,