feat: new layout, rooms row, fix weather+HA, fix BottomNav overflow
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s

- Remove TasksCard and SavingsCard from home tab
- New grid layout: lights+thermostat row 1, purifier+weather row 2
- Add RoomsRow component with room navigation
- Fix HA entity mapping: fan.zhimi_rmb1_9528_air_purifier → fan.air_purifier
- Add real entity aliases for HA route
- Fix weather route: add timeout, better error handling
- Fix BottomNav: use 100dvh + flex-shrink-0
- TopBar: accept isDemo prop, show Demo badge in header
- WeatherCard: compact prop, better loading/error states
- globals.css: add no-scrollbar utility
This commit is contained in:
Cosmo
2026-04-22 10:33:20 +00:00
parent ecf69400f6
commit 088cd35ea6
7 changed files with 375 additions and 152 deletions

View File

@@ -4,12 +4,17 @@ import { NextRequest, NextResponse } from "next/server";
const HA_URL = process.env.HA_URL || "http://192.168.31.110:8123";
const HA_TOKEN = process.env.HA_TOKEN || "";
// Mock data for demo mode
const MOCK_STATES = {
// Real entity IDs → normalized keys used in the dashboard
const REAL_ENTITY_ALIASES: Record<string, string> = {
"fan.zhimi_rmb1_9528_air_purifier": "fan.air_purifier",
};
// Mock data for entities that don't exist yet (квартира строится)
const MOCK_MISSING: Record<string, any> = {
"light.living_room": {
entity_id: "light.living_room",
state: "on",
attributes: { brightness: 180, friendly_name: "Свет Гостиная" },
state: "off",
attributes: { brightness: 0, friendly_name: "Свет Гостиная" },
},
"light.bedroom": {
entity_id: "light.bedroom",
@@ -18,9 +23,9 @@ const MOCK_STATES = {
},
"climate.thermostat": {
entity_id: "climate.thermostat",
state: "heat",
state: "off",
attributes: {
current_temperature: 21.5,
current_temperature: null,
temperature: 22,
friendly_name: "Термостат",
},
@@ -36,9 +41,16 @@ const MOCK_STATES = {
},
};
const RELEVANT_KEYS = [
"light.living_room",
"light.bedroom",
"climate.thermostat",
"fan.air_purifier",
];
export async function GET(req: NextRequest) {
if (!HA_TOKEN) {
return NextResponse.json({ demo: true, states: MOCK_STATES });
return NextResponse.json({ demo: true, states: MOCK_MISSING });
}
try {
@@ -51,25 +63,40 @@ export async function GET(req: NextRequest) {
});
if (!res.ok) throw new Error(`HA responded ${res.status}`);
const states = await res.json();
const relevant = [
"light.living_room",
"light.bedroom",
"climate.thermostat",
"fan.air_purifier",
];
const states: any[] = await res.json();
// Build filtered map with normalized keys
const filtered: Record<string, any> = {};
for (const s of states) {
if (relevant.includes(s.entity_id)) {
// Direct match
if (RELEVANT_KEYS.includes(s.entity_id)) {
filtered[s.entity_id] = s;
}
// Alias match (real entity → normalized key)
if (REAL_ENTITY_ALIASES[s.entity_id]) {
const normalizedKey = REAL_ENTITY_ALIASES[s.entity_id];
filtered[normalizedKey] = {
...s,
entity_id: normalizedKey,
_real_entity_id: s.entity_id,
};
}
}
return NextResponse.json({ demo: false, states: filtered });
// Fill in missing entities with mock data (but mark them)
for (const key of RELEVANT_KEYS) {
if (!filtered[key]) {
filtered[key] = { ...MOCK_MISSING[key], _mock: true };
}
}
// demo = true only if ALL entities are mock (no real HA devices connected)
const hasAnyReal = RELEVANT_KEYS.some(k => !filtered[k]?._mock);
return NextResponse.json({ demo: !hasAnyReal, states: filtered });
} catch (e) {
return NextResponse.json({ demo: true, states: MOCK_STATES });
// Fallback to full mock
return NextResponse.json({ demo: true, states: MOCK_MISSING });
}
}
@@ -80,6 +107,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: true, demo: true });
}
// Resolve alias: if normalized key, find real entity
const realEntityId = Object.keys(REAL_ENTITY_ALIASES).find(
k => REAL_ENTITY_ALIASES[k] === entity_id
) || entity_id;
try {
const res = await fetch(`${HA_URL}/api/services/${domain}/${service}`, {
method: "POST",
@@ -87,7 +119,7 @@ export async function POST(req: NextRequest) {
Authorization: `Bearer ${HA_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ entity_id, ...serviceData }),
body: JSON.stringify({ entity_id: realEntityId, ...serviceData }),
});
if (!res.ok) throw new Error(`HA responded ${res.status}`);