feat: add per-room temperature thresholds

Issue: #487
User-Visible: yes
This commit is contained in:
Matysh
2026-09-08 10:44:13 +03:00
parent d2d9b1ac5d
commit e2751062dc
73 changed files with 1465 additions and 864 deletions
+7 -1
View File
@@ -170,8 +170,14 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
* 289 556 Б; writer, шрифт, печатная сцена и общий дизайнерский арт мебели
* остались в `lazyPdfFiles` (114 486 Б gzip). Новый центр даёт 944 Б сверху и 1 056 Б
* снизу; PDF-граф не пересекается с initial View.
*
* 2026-09-07, #487: потолок поднят 290 500 → 291 500 без изменения общего
* бюджета. Pure-resolver диапазона комнаты и его вызовы нужны обоим View-
* renderer'ам до загрузки редактора: именно они меняют температурную заливку
* обычного и статичного плана. Измеренный факт 290 937 Б оставляет 563 Б
* сверху и 1 437 Б до нижней границы полосы; UI и writer остались в lazy editor.
*/
export const INITIAL_VIEW_GZIP_CEILING = 290_500;
export const INITIAL_VIEW_GZIP_CEILING = 291_500;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
+28
View File
@@ -245,6 +245,34 @@ export const CONFIG_FIELD_REGISTRY = Object.freeze([
migration: 'remove the fill token and materialize effective room glow on room edit or explicit optimization',
compatibility: 'backend and frontend accept the legacy token indefinitely',
},
{
id: 'spaces[].rooms[].settings.temp_min',
selector: { path: ['spaces', '*', 'rooms', '*', 'settings', 'temp_min'] },
storage: 'House Plan server config',
type: 'finite number | null',
default: 'unset/null (= inherit the lower comfort bound from the space)',
level: 'room',
ui: 'Room settings when the effective fill is Temperature',
runtime: 'roomTempRangeOf; affects temperature floor fill and opening tunnel fill only',
introduced: 'v1.73.0',
status: 'current',
migration: 'none — each absent/null bound inherits independently',
compatibility: 'older clients ignore the field and may erase it when rewriting complete room settings',
},
{
id: 'spaces[].rooms[].settings.temp_max',
selector: { path: ['spaces', '*', 'rooms', '*', 'settings', 'temp_max'] },
storage: 'House Plan server config',
type: 'finite number | null',
default: 'unset/null (= inherit the upper comfort bound from the space)',
level: 'room',
ui: 'Room settings when the effective fill is Temperature',
runtime: 'roomTempRangeOf; affects temperature floor fill and opening tunnel fill only',
introduced: 'v1.73.0',
status: 'current',
migration: 'none — each absent/null bound inherits independently',
compatibility: 'older clients ignore the field and may erase it when rewriting complete room settings',
},
{
id: 'spaces[].open_spans',
selector: { path: ['spaces', '*', 'open_spans'] },
+8
View File
@@ -1186,6 +1186,14 @@
"required": false,
"type": "float"
},
"config.spaces[].rooms[].settings.temp_max": {
"opaque": "<function _finite_number_or_none>",
"required": false
},
"config.spaces[].rooms[].settings.temp_min": {
"opaque": "<function _finite_number_or_none>",
"required": false
},
"config.spaces[].rooms[].settings.temp_source": {
"nullable": true,
"required": false,
+27 -5
View File
@@ -108,6 +108,28 @@ function relocateEditorPatch(patch, cardSource, editorSource) {
// `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда
// попало», проверяет не то, что объявлен проверять. Это контролирует --check.
const MUTANT_DEFINITIONS = [
{
id: 'room-temperature-blank-becomes-zero',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="room temperature draft" test/space-dialog.test.mjs',
because: 'a blank per-room bound means inheritance and must never become the explicit numeric value zero',
patches: [{
file: 'src/space-dialog.ts',
find: " if (String(raw ?? '').trim() === '') return { valid: true, value: null };",
replace: " if (String(raw ?? '').trim() === '') return { valid: true, value: 0 };",
}],
},
{
id: 'room-temperature-min-inherits-wrong-side',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="room temperature range" test/logic.test.mjs',
because: 'each absent room threshold must inherit the matching space side before the effective pair is normalized',
patches: [{
file: 'src/logic.ts',
find: ' const rawMin = finiteOr(room?.settings?.temp_min, inheritedMin);',
replace: ' const rawMin = finiteOr(room?.settings?.temp_min, inheritedMax);',
}],
},
{
id: 'pdf-shared-wall-twice',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
@@ -2521,13 +2543,13 @@ const MUTANT_DEFINITIONS = [
{
id: 'schema-manifest-enum-drift',
guard: 'node --test test/config-schema-parity.test.mjs',
because: 'a backend enum value the frontend does not know (and the allow-list does not '
+ 'bless) is exactly the schema drift #33 exists to catch — the manifest mutation '
+ 'simulates the backend change without the frontend pair',
because: 'a backend schema path the frontend does not know (and the allow-list does not '
+ 'bless) is exactly the schema drift #33 exists to catch; a single-line path mutation '
+ 'also stays deterministic across LF and CRLF schema dumps',
patches: [{
file: 'scripts/config-schema.json',
find: " \"config.spaces[].settings.fill_mode\": {\n \"enum\": [\n \"custom\",",
replace: " \"config.spaces[].settings.fill_mode\": {\n \"enum\": [\n \"phantom-33\",\n \"custom\",",
find: ' "config.spaces[].settings.fill_mode": {',
replace: ' "config.spaces[].settings.fill_mode_phantom": {',
}],
},
{
+9
View File
@@ -27,6 +27,15 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: [
'roomTempRangeOf', 'roomTempRangeFromDraft', 'roomTempThresholdDraft',
'applyRoomTempThresholdDraft', 'roomTemperatureControls',
],
smokes: ['smoke_room_temperature_thresholds.mjs'],
because: 'the #487 production-bundle scenario proves partial inheritance, zero/invalid input, '
+ 'hidden draft retention, reset and narrow-dialog overflow through the real room editor',
},
{
symbols: [
'PdfExportRuntime', 'buildPdfPage', 'writePdf', 'stableDimensionEdges',