new ui 1
This commit is contained in:
@@ -77,6 +77,19 @@ async def index(request: Request) -> HTMLResponse:
|
|||||||
return await dashboard(request)
|
return await dashboard(request)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rooms", response_class=HTMLResponse)
|
||||||
|
async def rooms(request: Request) -> HTMLResponse:
|
||||||
|
"""Render the rooms overview page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The FastAPI request object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTMLResponse: Rendered rooms template
|
||||||
|
"""
|
||||||
|
return templates.TemplateResponse("rooms.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/dashboard", response_class=HTMLResponse)
|
@app.get("/dashboard", response_class=HTMLResponse)
|
||||||
async def dashboard(request: Request) -> HTMLResponse:
|
async def dashboard(request: Request) -> HTMLResponse:
|
||||||
"""Render the dashboard with rooms and devices.
|
"""Render the dashboard with rooms and devices.
|
||||||
|
|||||||
188
apps/ui/redesign_ui.txt
Normal file
188
apps/ui/redesign_ui.txt
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge eine neue Home-Dashboard-Seite mit Raum-Kacheln.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
Die Seite soll alle Räume als kleine Kacheln darstellen. Auf dem iPhone
|
||||||
|
sollen immer zwei Kacheln nebeneinander passen. Jede Kachel zeigt:
|
||||||
|
- Raumname
|
||||||
|
- Icon (z. B. Wohnzimmer, Küche, Bad, etc.) basierend auf room_id oder einem Mapping
|
||||||
|
- Anzahl der Geräte im Raum
|
||||||
|
- Optional: Zusammenfassung wichtiger States (z.B. Anzahl offener Fenster, aktive Lichter)
|
||||||
|
|
||||||
|
Datenquelle:
|
||||||
|
- GET /layout → { "rooms": [{ "name": "...", "devices": [...] }] }
|
||||||
|
(Achtung: rooms ist ein Array, kein Dictionary!)
|
||||||
|
- GET /devices → Geräteliste für Feature-Checks
|
||||||
|
|
||||||
|
Interaktion:
|
||||||
|
- Beim Klick/Touch auf eine Raum-Kachel → Navigation zu /room/{room_name}
|
||||||
|
|
||||||
|
Layout-Anforderungen:
|
||||||
|
- 2-Spalten-Grid auf kleinen Screens (max-width ~ 600px)
|
||||||
|
- 3–4 Spalten auf größeren Screens
|
||||||
|
- Kachelgröße kompakt (ca. 140px x 110px)
|
||||||
|
- Icon ~32px
|
||||||
|
- Text ~14–16px
|
||||||
|
- Responsive via CSS-Grid oder Flexbox
|
||||||
|
- Minimaler Einsatz von Tailwind (bevorzugt vanilla CSS)
|
||||||
|
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
- Die Seite lädt alle Räume über die API (fetch).
|
||||||
|
- Räume werden in der Reihenfolge aus layout.yaml angezeigt.
|
||||||
|
- Jede Kachel zeigt: Icon + Raumname + Geräteanzahl.
|
||||||
|
- iPhone-Darstellung verifiziert: zwei Kacheln nebeneinander.
|
||||||
|
- Funktionierende Navigation zu /room/{room_name}.
|
||||||
|
- Die Komponente ist vollständig lauffähig.
|
||||||
|
- Fehlerbehandlung bei API-Fehlern implementiert.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge eine Geräte-Grid-Ansicht für einen Raum.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
Die Seite zeigt alle Geräte, die in diesem Raum laut layout.yaml liegen.
|
||||||
|
Die Darstellung erfolgt als kompakte Kacheln, ebenfalls 2 Spalten auf iPhone.
|
||||||
|
|
||||||
|
Datenquelle:
|
||||||
|
- GET /layout → Räume + device_id + title
|
||||||
|
- GET /devices → Typ + Features
|
||||||
|
- GET /devices/{id}/state (optional zur Initialisierung)
|
||||||
|
- Live-Updates: SSE /realtime
|
||||||
|
|
||||||
|
Auf einer Gerät-Kachel sollen erscheinen:
|
||||||
|
- passendes Icon (abhängig von type)
|
||||||
|
- title (aus layout)
|
||||||
|
- wichtigste Eigenschaft aus dem State:
|
||||||
|
- light: power on/off oder brightness in %
|
||||||
|
- thermostat: current temperature
|
||||||
|
- contact: open/closed
|
||||||
|
- temp_humidity: temperature und/oder humidity
|
||||||
|
- outlet: on/off
|
||||||
|
- cover: position %
|
||||||
|
|
||||||
|
Interaktion:
|
||||||
|
- Klick/Touch → Navigation zu /device/{device_id}
|
||||||
|
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
- Der Raum wird anhand room_id aus der URL geladen.
|
||||||
|
- Geräte werden über Join(layout, devices) des Raums selektiert.
|
||||||
|
- Kacheln sind 2-spaltig auf iPhone.
|
||||||
|
- State wird initial geladen und per SSE aktualisiert.
|
||||||
|
- Navigation zu /device/{id} funktioniert.
|
||||||
|
- Icons passend zum Typ generiert.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge eine Detailansicht für ein einzelnes Gerät.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
Die Seite zeigt:
|
||||||
|
- Titel des Geräts (title aus layout)
|
||||||
|
- Raumname
|
||||||
|
- Gerätetyp
|
||||||
|
- State-Werte aus GET /devices/{id}/state
|
||||||
|
- Live-Updates via SSE
|
||||||
|
- Steuer-Elemente abhängig vom type + features:
|
||||||
|
- light: toggle, brightness-slider, optional color-picker
|
||||||
|
- thermostat: target-temp-slider
|
||||||
|
- outlet: toggle
|
||||||
|
- contact: nur Anzeige
|
||||||
|
- temp_humidity: nur Anzeigen von Temperatur/Humidity
|
||||||
|
- cover: position-slider und open/close/stop Buttons
|
||||||
|
|
||||||
|
API-Integration:
|
||||||
|
- Set-Kommandos senden via POST /devices/{id}/set
|
||||||
|
- Validierung: Nur unterstützte Features sichtbar machen
|
||||||
|
|
||||||
|
UI-Vorgaben:
|
||||||
|
- Kompakt, aber komplett
|
||||||
|
- Buttons gut für Touch erreichbar
|
||||||
|
- Slider in voller Breite
|
||||||
|
- Werte (temperature, humidity, battery) übersichtlich gruppiert
|
||||||
|
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
- Device wird korrekt geladen (layout + devices + state).
|
||||||
|
- Steuerung funktioniert (light on/off, brightness, target temp etc.).
|
||||||
|
- SSE aktualisiert alle angezeigten Werte live.
|
||||||
|
- Fehler (z. B. POST /set nicht erreichbar) werden UI-seitig angezeigt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge einen API-Client für das UI.
|
||||||
|
|
||||||
|
Der Client soll bereitstellen:
|
||||||
|
- getLayout(): Layout-Daten
|
||||||
|
- getDevices(): Device-Basisdaten
|
||||||
|
- getDeviceState(device_id)
|
||||||
|
- setDeviceState(device_id, type, payload)
|
||||||
|
- connectRealtime(onEvent): SSE-Listener
|
||||||
|
|
||||||
|
Anforderungen:
|
||||||
|
- API_BASE aus .env oder UI-Konfiguration
|
||||||
|
- Fehlerbehandlung
|
||||||
|
- Timeout optional
|
||||||
|
- Types für:
|
||||||
|
- Room
|
||||||
|
- Device
|
||||||
|
- DeviceState
|
||||||
|
- RealtimeEvent
|
||||||
|
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
- Der Client ist voll funktionsfähig und wird im UI genutzt.
|
||||||
|
- Ein Hook useRealtime(device_id) wird erzeugt.
|
||||||
|
- Ein Hook useRooms() and useDevices() existieren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge das UI-Routing.
|
||||||
|
|
||||||
|
Routen:
|
||||||
|
- "/" → Home (Räume)
|
||||||
|
- "/room/:roomId" → RoomView
|
||||||
|
- "/device/:deviceId" → DeviceView
|
||||||
|
|
||||||
|
Anforderungen:
|
||||||
|
- React Router v6 oder v7
|
||||||
|
- Layout-Komponente optional
|
||||||
|
- Loading/Fehlerzustände
|
||||||
|
- Responsive Verhalten beibehalten
|
||||||
|
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
- Navigation funktioniert zwischen allen Seiten.
|
||||||
|
- Browser-Back funktioniert erwartungsgemäß.
|
||||||
|
- Routes unterstützen Refresh ohne Fehler.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Implementiere einen React-Hook useRealtime(deviceId: string | null).
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
- SSE-Stream /realtime abonnieren
|
||||||
|
- Nur Events für deviceId liefern
|
||||||
|
- onMessage → setState
|
||||||
|
- automatische Reconnects
|
||||||
|
- Fehlerlogging
|
||||||
|
|
||||||
|
Akzeptanz:
|
||||||
|
- Der Hook kann in RoomView & DeviceView genutzt werden.
|
||||||
|
- Live-Updates werden korrekt gemerged.
|
||||||
|
- Disconnect/Reload funktioniert sauber.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Copilot-Aufgabe: Erzeuge eine Icon-Komponente.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
Basierend auf device.type und ggf. features ein passendes SVG ausliefern:
|
||||||
|
- light → Lightbulb
|
||||||
|
- thermostat → Thermostat
|
||||||
|
- contact → Door/Window-Sensor
|
||||||
|
- temp_humidity → Thermometer+Droplet
|
||||||
|
- outlet → Power-Plug
|
||||||
|
- cover → Blinds/Rollershutter
|
||||||
|
|
||||||
|
Akzeptanz:
|
||||||
|
- Icons skalieren sauber
|
||||||
|
- funktionieren in allen Kachel-Komponenten
|
||||||
|
*/
|
||||||
|
|
||||||
306
apps/ui/templates/rooms.html
Normal file
306
apps/ui/templates/rooms.html
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Räume - Home Automation</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: white;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.rooms-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.rooms-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
min-height: 110px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-card:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-device-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(255, 59, 48, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #667eea;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/" class="back-button">← Dashboard</a>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏠 Räume</h1>
|
||||||
|
|
||||||
|
<div id="error-container"></div>
|
||||||
|
<div id="loading" class="loading">Lade Räume...</div>
|
||||||
|
<div id="rooms-grid" class="rooms-grid" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = window.location.origin;
|
||||||
|
|
||||||
|
// Room icon mapping
|
||||||
|
const roomIcons = {
|
||||||
|
'wohnzimmer': '🛋️',
|
||||||
|
'küche': '🍳',
|
||||||
|
'kueche': '🍳',
|
||||||
|
'schlafzimmer': '🛏️',
|
||||||
|
'bad': '🚿',
|
||||||
|
'badezimmer': '🚿',
|
||||||
|
'bad oben': '🚿',
|
||||||
|
'bad unten': '🚿',
|
||||||
|
'flur': '🚪',
|
||||||
|
'büro': '💼',
|
||||||
|
'buero': '💼',
|
||||||
|
'arbeitszimmer': '💼',
|
||||||
|
'studierzimmer': '📚',
|
||||||
|
'esszimmer': '🍽️',
|
||||||
|
'garten': '🌳',
|
||||||
|
'terrasse': '🌿',
|
||||||
|
'garage': '🚗',
|
||||||
|
'keller': '🔧',
|
||||||
|
'dachboden': '📦',
|
||||||
|
'kinderzimmer': '🧸',
|
||||||
|
'patty': '👤',
|
||||||
|
'wolfgang': '👤',
|
||||||
|
'default': '🏡'
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRoomIcon(roomName) {
|
||||||
|
const normalized = roomName.toLowerCase().trim();
|
||||||
|
return roomIcons[normalized] || roomIcons['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRooms() {
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const grid = document.getElementById('rooms-grid');
|
||||||
|
const errorContainer = document.getElementById('error-container');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load layout
|
||||||
|
const layoutResponse = await fetch(`${API_BASE}/layout`);
|
||||||
|
if (!layoutResponse.ok) {
|
||||||
|
throw new Error(`Layout API error: ${layoutResponse.status}`);
|
||||||
|
}
|
||||||
|
const layoutData = await layoutResponse.json();
|
||||||
|
|
||||||
|
// Load devices for feature checks
|
||||||
|
const devicesResponse = await fetch(`${API_BASE}/devices`);
|
||||||
|
if (!devicesResponse.ok) {
|
||||||
|
throw new Error(`Devices API error: ${devicesResponse.status}`);
|
||||||
|
}
|
||||||
|
const devicesData = await devicesResponse.json();
|
||||||
|
|
||||||
|
// Create device lookup
|
||||||
|
const deviceMap = {};
|
||||||
|
devicesData.forEach(device => {
|
||||||
|
deviceMap[device.device_id] = device;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render rooms
|
||||||
|
loading.style.display = 'none';
|
||||||
|
grid.style.display = 'grid';
|
||||||
|
|
||||||
|
layoutData.rooms.forEach(room => {
|
||||||
|
const card = createRoomCard(room, deviceMap);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading rooms:', error);
|
||||||
|
loading.style.display = 'none';
|
||||||
|
errorContainer.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
⚠️ Fehler beim Laden der Räume: ${error.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoomCard(room, deviceMap) {
|
||||||
|
const card = document.createElement('a');
|
||||||
|
card.className = 'room-card';
|
||||||
|
card.href = `/room/${encodeURIComponent(room.name)}`;
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'room-icon';
|
||||||
|
icon.textContent = getRoomIcon(room.name);
|
||||||
|
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'room-name';
|
||||||
|
name.textContent = room.name;
|
||||||
|
|
||||||
|
const deviceCount = document.createElement('div');
|
||||||
|
deviceCount.className = 'room-device-count';
|
||||||
|
deviceCount.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
|
||||||
|
|
||||||
|
// Optional: Calculate stats (lights on, windows open, etc.)
|
||||||
|
const stats = calculateRoomStats(room.devices, deviceMap);
|
||||||
|
if (stats) {
|
||||||
|
const statsDiv = document.createElement('div');
|
||||||
|
statsDiv.className = 'room-stats';
|
||||||
|
statsDiv.textContent = stats;
|
||||||
|
card.appendChild(icon);
|
||||||
|
card.appendChild(name);
|
||||||
|
card.appendChild(deviceCount);
|
||||||
|
card.appendChild(statsDiv);
|
||||||
|
} else {
|
||||||
|
card.appendChild(icon);
|
||||||
|
card.appendChild(name);
|
||||||
|
card.appendChild(deviceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRoomStats(deviceIds, deviceMap) {
|
||||||
|
// Count device types
|
||||||
|
let lights = 0;
|
||||||
|
let thermostats = 0;
|
||||||
|
let contacts = 0;
|
||||||
|
let sensors = 0;
|
||||||
|
|
||||||
|
deviceIds.forEach(deviceId => {
|
||||||
|
const device = deviceMap[deviceId];
|
||||||
|
if (!device) return;
|
||||||
|
|
||||||
|
switch(device.type) {
|
||||||
|
case 'light':
|
||||||
|
lights++;
|
||||||
|
break;
|
||||||
|
case 'thermostat':
|
||||||
|
thermostats++;
|
||||||
|
break;
|
||||||
|
case 'contact':
|
||||||
|
contacts++;
|
||||||
|
break;
|
||||||
|
case 'temp_humidity_sensor':
|
||||||
|
sensors++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build compact stats string
|
||||||
|
const parts = [];
|
||||||
|
if (lights > 0) parts.push(`💡${lights}`);
|
||||||
|
if (thermostats > 0) parts.push(`🌡️${thermostats}`);
|
||||||
|
if (contacts > 0) parts.push(`🚪${contacts}`);
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load rooms on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', loadRooms);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user