diff --git a/custom_components/plantrun/www/plantrun-panel-styles.js b/custom_components/plantrun/www/plantrun-panel-styles.js index f618e43..7a534cb 100644 --- a/custom_components/plantrun/www/plantrun-panel-styles.js +++ b/custom_components/plantrun/www/plantrun-panel-styles.js @@ -111,28 +111,78 @@ export const panelStyles = ` .page-heading { display:flex; justify-content:space-between; gap:30px; align-items:end; margin-bottom:28px; } .page-heading>div { display:grid; gap:9px; max-width:730px; } .page-heading p { max-width:660px; } - .tent-strip { border-top:1px solid var(--line); border-bottom:1px solid var(--line); display:grid; grid-template-columns:repeat(4,1fr); margin:0 0 34px; } - .tent-reading { min-width:0; padding:18px 20px; display:grid; gap:5px; border-right:1px solid var(--line); } - .tent-reading:last-child { border:0; } - .tent-reading span,.tent-reading small { color:var(--muted); } - .tent-reading span { font-size:12px; } - .tent-reading strong { font-size:22px; overflow:hidden; text-overflow:ellipsis; } - .tent-reading small { font-size:10px; } - .plant-gallery { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:clamp(20px,3vw,36px); } - .plant-card { min-width:0; background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--radius-work); overflow:hidden; position:relative; } - .plant-card-main { width:100%; border:0; padding:0; background:transparent; color:inherit; cursor:pointer; text-align:left; display:grid; grid-template-columns:minmax(180px,42%) 1fr; } - .plant-card-photo { width:100%; height:330px; object-fit:cover; background:var(--surface-2); } - .photo-empty { display:grid; place-items:center; align-content:center; gap:10px; color:var(--quiet); } - .photo-empty ha-icon { --mdc-icon-size:54px; } - .photo-empty span { font-size:12px; } - .plant-card-body { padding:30px; display:flex; flex-direction:column; align-items:flex-start; min-width:0; } - .plant-card-body>strong { font-size:clamp(24px,3vw,38px); line-height:1.05; letter-spacing:-.035em; margin:12px 0 6px; } - .plant-card-body>small { color:var(--muted); } - .stage-label { display:inline-flex; align-items:center; min-height:29px; padding:0 10px; border:1px solid color-mix(in srgb,var(--accent) 55%,var(--line)); border-radius:999px; color:var(--accent-strong); font-size:11px; font-weight:800; } - .plant-card-meta { margin-top:auto; padding-top:25px; border-top:1px solid var(--line); width:100%; display:grid; grid-template-columns:auto 1fr; gap:22px; color:var(--muted); font-size:12px; } - .plant-card-meta>span:last-child { display:grid; gap:3px; } - .plant-card-meta b { color:var(--text); font-size:10px; text-transform:uppercase; letter-spacing:.08em; } - .journal-direct { position:absolute; right:18px; bottom:18px; min-height:39px; padding:0 13px; display:flex; align-items:center; gap:6px; border-radius:var(--radius-control); background:var(--accent-strong); color:var(--accent-ink); font-weight:760; } + .modern-sensor-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:12px; margin:0 0 28px; } + .sensor-card { background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--radius-group); padding:14px 16px; display:flex; flex-direction:column; gap:8px; transition:border-color .2s, transform .2s; } + .sensor-card:hover { border-color:var(--line); transform:translateY(-1px); } + .sensor-card-head { display:flex; align-items:center; justify-content:space-between; gap:6px; } + .sensor-card-title { display:flex; align-items:center; gap:7px; color:var(--muted); font-size:11.5px; font-weight:600; } + .sensor-card-icon { width:26px; height:26px; border-radius:7px; background:var(--surface-2); color:var(--accent-strong); display:grid; place-items:center; } + .sensor-card-icon ha-icon { --mdc-icon-size:16px; } + .sensor-card-val { font-size:21px; font-weight:750; letter-spacing:-.02em; color:var(--text); line-height:1.1; margin:2px 0 0; } + .sensor-card-sub { font-size:10px; color:var(--muted); display:flex; justify-content:space-between; align-items:center; margin-top:2px; } + + /* Zone badge styling */ + .zone-badge { display:inline-flex; align-items:center; gap:5px; font-size:10.5px; font-weight:700; padding:2.5px 8px; border-radius:6px; line-height:1.2; } + .zone-badge.good { background:rgba(52, 211, 153, 0.12); color:#34d399; border:1px solid rgba(52, 211, 153, 0.28); } + .zone-badge.warn { background:rgba(251, 191, 36, 0.12); color:#fbbf24; border:1px solid rgba(251, 191, 36, 0.28); } + .zone-badge.bad { background:rgba(248, 113, 113, 0.12); color:#f87171; border:1px solid rgba(248, 113, 113, 0.28); } + .zone-badge.neutral { background:rgba(255, 255, 255, 0.06); color:var(--muted); border:1px solid var(--line-soft); } + .zone-dot { width:5px; height:5px; border-radius:50%; background:currentColor; } + + /* Range Gauge Bar */ + .range-gauge-wrap { display:flex; flex-direction:column; gap:4px; width:100%; margin-top:5px; } + .range-gauge-track { position:relative; width:100%; height:5px; border-radius:5px; background:rgba(255, 255, 255, 0.08); overflow:visible; } + .theme-light .range-gauge-track { background:rgba(0, 0, 0, 0.08); } + .range-gauge-zone { position:absolute; top:0; bottom:0; background:linear-gradient(90deg, rgba(52, 211, 153, 0.35), rgba(74, 222, 128, 0.65)); border-radius:3px; } + .range-gauge-needle { position:absolute; top:50%; transform:translate(-50%, -50%); width:10px; height:10px; display:grid; place-items:center; z-index:2; } + .range-needle-dot { width:7px; height:7px; border-radius:50%; background:#ffffff; border:1.5px solid #10b981; box-shadow:0 0 5px rgba(255, 255, 255, 0.85); } + .range-gauge-labels { display:flex; justify-content:space-between; align-items:center; font-size:9px; color:var(--muted); } + .range-target-label { color:#34d399; font-weight:600; } + .theme-light .range-target-label { color:#15803d; } + + /* Modern Plant Cards */ + .plant-gallery { display:grid; grid-template-columns:repeat(auto-fit, minmax(380px, 1fr)); gap:clamp(16px, 2.5vw, 24px); } + .modern-plant-card { background:var(--surface); border:1px solid var(--line-soft); border-radius:var(--radius-work); padding:20px; display:flex; flex-direction:column; gap:14px; position:relative; transition:border-color .2s; } + .modern-plant-card:hover { border-color:var(--line); } + .plant-card-header { display:grid; grid-template-columns:116px 1fr; gap:16px; align-items:start; } + .plant-thumb-btn { position:relative; width:116px; height:116px; border-radius:var(--radius-control); overflow:hidden; background:var(--surface-2); border:0; padding:0; cursor:pointer; } + .plant-thumb-img { width:100%; height:100%; object-fit:cover; display:block; } + .plant-breeder-tag { position:absolute; bottom:5px; left:5px; right:5px; padding:2px 5px; border-radius:5px; background:rgba(5, 12, 7, 0.85); backdrop-filter:blur(4px); color:#d1fae5; font-size:9px; font-weight:700; text-align:center; border:1px solid rgba(255, 255, 255, 0.15); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .plant-info-block { display:flex; flex-direction:column; gap:5px; min-width:0; } + .plant-title-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } + .plant-stage-badge { display:inline-flex; align-items:center; gap:5px; padding:3px 8px; border-radius:6px; background:rgba(52, 211, 153, 0.12); color:#34d399; border:1px solid rgba(52, 211, 153, 0.28); font-size:10.5px; font-weight:750; width:fit-content; } + .pulse-dot { width:6px; height:6px; border-radius:50%; background:#34d399; box-shadow:0 0 6px #34d399; } + .plant-title-btn { border:0; background:transparent; padding:0; cursor:pointer; color:var(--text); font-size:21px; font-weight:750; letter-spacing:-.025em; text-align:left; line-height:1.2; } + .plant-title-btn:hover { color:var(--accent-strong); } + .plant-genetics-sub { color:var(--muted); font-size:11.5px; margin:0; line-height:1.35; } + .plant-harvest-pill { display:inline-flex; align-items:center; gap:5px; padding:3px 8px; border-radius:6px; background:var(--surface-2); color:var(--muted); font-size:10.5px; width:fit-content; margin-top:2px; } + .plant-harvest-pill ha-icon { --mdc-icon-size:13px; color:var(--muted); } + .plant-harvest-pill strong { color:var(--text); } + + /* Stage timeline */ + .stage-timeline { display:grid; grid-template-columns:repeat(5, 1fr); gap:6px; margin:4px 0 8px; } + .stage-step { display:flex; flex-direction:column; gap:4px; } + .stage-step-bar { height:4px; border-radius:3px; background:var(--line); } + .stage-step.done .stage-step-bar { background:rgba(74, 222, 128, 0.6); } + .stage-step.active .stage-step-bar { background:var(--accent-strong); box-shadow:0 0 8px rgba(74, 222, 128, 0.35); } + .stage-step small { font-size:9.5px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .stage-step.active small { color:var(--text); font-weight:700; } + + /* Plant Telemetry 3-col Grid */ + .plant-telemetry-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; padding:12px; background:var(--surface-2); border-radius:var(--radius-control); border:1px solid var(--line-soft); } + .telemetry-cell { display:flex; flex-direction:column; gap:4px; min-width:0; } + .telemetry-cell-head { display:flex; align-items:center; justify-content:space-between; color:var(--muted); font-size:10.5px; font-weight:600; gap:4px; } + .telemetry-cell-title { display:inline-flex; align-items:center; gap:5px; } + .telemetry-cell-title ha-icon { --mdc-icon-size:14px; opacity:.85; color:var(--accent-strong); } + .telemetry-cell-val { font-size:17px; font-weight:750; color:var(--text); line-height:1.1; } + + /* Card Bottom Action Bar */ + .plant-card-foot { display:flex; align-items:center; justify-content:space-between; gap:12px; padding-top:6px; border-top:1px solid var(--line-soft); flex-wrap:wrap; } + .plant-care-status { display:flex; align-items:center; gap:6px; color:var(--muted); font-size:11px; } + .plant-care-status ha-icon { --mdc-icon-size:14px; color:var(--accent-strong); } + .plant-card-actions { display:flex; align-items:center; gap:8px; } + .plant-card-actions .primary { min-height:34px; padding:0 12px; font-size:12px; } + .plant-card-actions .secondary { min-height:34px; padding:0 10px; font-size:12px; } .empty-garden,.system-state,.empty-journal { min-height:360px; border:1px dashed var(--line); border-radius:var(--radius-work); display:grid; align-content:center; justify-items:center; gap:12px; text-align:center; padding:34px; } .empty-garden ha-icon,.system-state ha-icon { --mdc-icon-size:52px; color:var(--accent); } .empty-garden p,.system-state p,.empty-journal p { max-width:500px; } diff --git a/custom_components/plantrun/www/plantrun-panel-views.js b/custom_components/plantrun/www/plantrun-panel-views.js index bf63dbd..5f02327 100644 --- a/custom_components/plantrun/www/plantrun-panel-views.js +++ b/custom_components/plantrun/www/plantrun-panel-views.js @@ -114,6 +114,44 @@ export function createPanelViewMethods() { return this._renderOverview(); }, + _renderRangeBar(value, min, max, targetLow, targetHigh, unit = "") { + const numVal = Number(value); + if (!Number.isFinite(numVal)) return ""; + const targetLowPct = Math.max(0, Math.min(100, ((targetLow - min) / (max - min)) * 100)); + const targetWidthPct = Math.max(2, Math.min(100 - targetLowPct, ((targetHigh - targetLow) / (max - min)) * 100)); + const currentPct = Math.max(0, Math.min(100, ((numVal - min) / (max - min)) * 100)); + return `
+
+
+
+ +
+
+
+ ${min}${unit} + Ziel ${targetLow}–${targetHigh}${unit} + ${max}${unit} +
+
`; + }, + + _renderStatusBadge(value, targetLow, targetHigh, targetText = "") { + const numVal = Number(value); + if (!Number.isFinite(numVal)) { + return `Bereit`; + } + let status = "good"; + let label = targetText || "Optimal"; + if (numVal < targetLow) { + status = "warn"; + label = "Niedrig"; + } else if (numVal > targetHigh) { + status = "warn"; + label = "Erhöht"; + } + return `${e(label)}`; + }, + _renderOverview() { const tent = this._activeTent(); const runs = this._state.runs.filter((run) => !isArchived(run)); @@ -128,31 +166,157 @@ export function createPanelViewMethods() { }, _renderTentStrip(tent) { - const metrics = ["temperature", "humidity", "light", "energy"]; + const metricConfigs = [ + { + key: "temperature", + label: "Temperatur", + icon: "mdi:thermometer", + min: 18, + max: 34, + targetLow: 24, + targetHigh: 28, + unit: "°C", + fallbackSub: "Min 22,4 °C · Max 27,2 °C" + }, + { + key: "humidity", + label: "Luftfeuchte", + icon: "mdi:water-percent", + min: 40, + max: 90, + targetLow: 65, + targetHigh: 75, + unit: "%", + fallbackSub: "Min 61,0 % · Max 72,0 %" + }, + { + key: "light", + label: "Licht", + icon: "mdi:white-balance-sunny", + min: 0, + max: 80, + targetLow: 40, + targetHigh: 65, + unit: " klx", + fallbackSub: "18/6h Lichtplan" + }, + { + key: "energy", + label: "Energie", + icon: "mdi:flash-outline", + min: 0, + max: 400, + targetLow: 200, + targetHigh: 320, + unit: " W", + fallbackSub: "Laufender Verbrauch" + } + ]; + const bindings = Array.isArray(tent?.bindings) ? tent.bindings : []; - const rows = metrics.map((metric) => { - const binding = bindings.find((item) => item?.metric_type === metric && !item?.ended_at); + const cards = metricConfigs.map((cfg) => { + const binding = bindings.find((item) => item?.metric_type === cfg.key && !item?.ended_at); const entityId = entityIdFor(binding); const live = this._entityDisplay(entityId); - const label = metric === "temperature" ? "Temperatur" : metric === "humidity" ? "Luftfeuchte" : metric === "light" ? "Licht" : "Energie"; - return `
${label}${e(live.value)}${entityId ? e(live.status) : "Nicht zugeordnet"}
`; + const numeric = Number(live.raw); + const hasLive = Number.isFinite(numeric); + return `
+
+ ${cfg.label} + ${!entityId ? `Nicht zugeordnet` : ""} +
+
${e(live.value)}
+ ${hasLive ? this._renderRangeBar(numeric, cfg.min, cfg.max, cfg.targetLow, cfg.targetHigh, cfg.unit) : ""} +
${entityId ? e(live.status) : cfg.fallbackSub}
+
`; }); - return `
${rows.join("")}
`; + + return `
${cards.join("")}
`; }, _renderPlantCard(run) { const stage = currentStage(run); - return `
- - + const day = daysSince(runStart(run)); + const stages = ["Keimung", "Sämling", "Vegetativ", "Blüte", "Ernte"]; + const plan = stagePlan(run); + const recorded = recordedStages(run); + const breeder = breederName(run); + const cultivar = cultivarName(run); + const container = typeof run?.plant?.container === "string" ? run.plant.container : run?.plant?.container?.label || run?.container; + const substrate = run?.plant?.substrate || run?.substrate || ""; + + const tent = this._activeTent(); + const moistureBinding = bindingForMetric(run, tent, "soil_moisture"); + const tempBinding = bindingForMetric(run, tent, "temperature"); + const ecBinding = bindingForMetric(run, tent, "conductivity"); + + const moistureLive = this._entityDisplay(entityIdFor(moistureBinding)); + const tempLive = this._entityDisplay(entityIdFor(tempBinding)); + const ecLive = this._entityDisplay(entityIdFor(ecBinding)); + + const moistureVal = Number(moistureLive.raw); + const tempVal = Number(tempLive.raw); + const ecVal = Number(ecLive.raw); + + return `
+
+ +
+
+ ${e(stage)} · Tag ${day} +
+

+

${e(cultivar)}${breeder ? ` · ${e(breeder)}` : ""}${container ? ` · ${e(container)}` : ""}${substrate ? ` (${e(substrate)})` : ""}

+ Nächste Schätzung:${e(harvestEstimate(run))} +
+
+ +
+ ${(plan.length ? plan : stages).map((stg) => { + const isCurrent = stg.toLowerCase() === stage.toLowerCase(); + const isPast = recorded.has(stg.toLowerCase()); + const cls = isCurrent ? "active" : isPast ? "done" : "future"; + return `
+
+ ${e(stg)} +
`; + }).join("")} +
+ +
+
+
+ Bodenfeuchte +
+
${e(moistureLive.value)}
+ ${Number.isFinite(moistureVal) ? this._renderRangeBar(moistureVal, 20, 80, 45, 65, "%") : `
${moistureLive.value !== "–" ? e(moistureLive.value) : "Kein Sensor zugeordnet"}
`} +
+
+
+ Substrat-Temp +
+
${e(tempLive.value)}
+ ${Number.isFinite(tempVal) ? this._renderRangeBar(tempVal, 15, 32, 20, 26, "°C") : `
${tempLive.value !== "–" ? e(tempLive.value) : "Kein Sensor zugeordnet"}
`} +
+
+
+ Leitfähigkeit (EC) +
+
${e(ecLive.value)}
+ ${Number.isFinite(ecVal) ? this._renderRangeBar(ecVal, 0.0, 2.4, 0.8, 1.4, " mS") : `
${ecLive.value !== "–" ? e(ecLive.value) : "Kein Sensor zugeordnet"}
`} +
+
+ +
+ ${run.journal_entries?.[0] ? `Letzter Eintrag: ${e(formatDate(run.journal_entries[0].occurred_at, true))}` : "Bereit für ersten Eintrag"} +
+ + +
+
`; }, diff --git a/custom_components/plantrun/www/prototype/plantrun-ui-prototype.html b/custom_components/plantrun/www/prototype/plantrun-ui-prototype.html index a86a0fd..b5598c4 100644 --- a/custom_components/plantrun/www/prototype/plantrun-ui-prototype.html +++ b/custom_components/plantrun/www/prototype/plantrun-ui-prototype.html @@ -89,83 +89,126 @@ .fact dt { color: var(--muted); } .fact dd { margin: 0; font-weight: 700; text-align: right; } - /* A. Two equal, image-first plants. */ + /* A. Two equal, image-first plants - Modernized */ .focus { min-height: 100vh; - --bg: #f1f7ef; - --surface: #f8fbf6; - --surface-2: #e5efdf; - --surface-3: #dbe8d5; - --text: #172018; - --muted: #69746a; - --line: rgba(31, 55, 34, .12); - --line-strong: rgba(31, 55, 34, .21); - --accent: #72b94f; - --accent-soft: #4c8039; - --accent-ink: #f8fff4; - --shadow: none; + --bg: #070d09; + --surface: #0f1912; + --surface-2: #152219; + --surface-3: #1d3023; + --text: #f0f7f2; + --muted: #8fa596; + --line: rgba(144, 238, 144, 0.09); + --line-strong: rgba(144, 238, 144, 0.2); + --accent: #4ade80; + --accent-soft: #86efac; + --accent-ink: #07190d; + --accent-glow: rgba(74, 222, 128, 0.25); + --shadow: 0 16px 48px rgba(0, 0, 0, .45); background: var(--bg); color: var(--text); } - html[data-theme="dark"] .focus { - --bg: #141713; - --surface: #191e18; - --surface-2: #232a21; - --surface-3: #2b3429; - --text: #eff2ea; - --muted: #9ba397; - --line: rgba(225, 235, 219, .12); - --line-strong: rgba(225, 235, 219, .21); - --accent: #7ea268; - --accent-soft: #acc69d; - --accent-ink: #11180e; + html[data-theme="light"] .focus { + --bg: #f2f7f2; + --surface: #ffffff; + --surface-2: #eaf2e9; + --surface-3: #ddeadd; + --text: #132217; + --muted: #5e7364; + --line: rgba(22, 54, 29, .1); + --line-strong: rgba(22, 54, 29, .22); + --accent: #2e9e50; + --accent-soft: #237b3e; + --accent-ink: #f0fbf2; + --accent-glow: rgba(46, 158, 80, 0.2); } .focus-shell { display: grid; grid-template-columns: 82px minmax(0, 1fr); min-height: 100vh; } - .focus .side-nav { background: transparent; } - .focus .brand { border: 1px solid var(--line-strong); border-radius: 5px; background: transparent; color: var(--accent-soft); } - .focus .nav-link { border-radius: 0; background: transparent; } - .focus .nav-link.active { box-shadow: inset 2px 0 var(--accent); background: transparent; color: var(--text); } - .focus-main { padding: 26px clamp(22px, 3.6vw, 58px) 120px; } - .focus .page-header h1 { font-size: clamp(27px, 2.5vw, 36px); font-weight: 620; } - .focus .button, .focus .icon-button, .focus .language { border-radius: 6px; } - .focus .language button { border-radius: 3px; } - .focus .header-controls .button.primary { border: 1px solid var(--line-strong); background: transparent; color: var(--text); font-weight: 620; } - .focus .sensor-ribbon { gap: 0; margin: 22px 0 30px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } - .focus .sensor-pill { border: 0; border-right: 1px solid var(--line); border-radius: 0; background: transparent; } - .focus .sensor-pill:last-child { border-right: 0; } - .focus .sensor-icon { background: transparent; color: var(--muted); } - .focus .sensor-pill strong { font-weight: 600; } - .focus-layout { display: grid; grid-template-columns: minmax(0, 1fr) 290px; gap: 0; } - .focus-plants { display: grid; grid-template-columns: 1fr 1fr; gap: 0; min-width: 0; } - .focus-card { min-width: 0; padding: 0 clamp(18px, 2.5vw, 36px); border-right: 1px solid var(--line); background: transparent; } - .focus-card:first-child { padding-left: 0; } - .focus-card:last-child { border-right: 0; } - .focus-image { position: relative; display: block; width: 100%; height: clamp(260px, 29vw, 390px); overflow: hidden; padding: 0; border-radius: 22px; background: transparent; text-align: left; } - .focus-image img { width: 100%; height: 100%; object-fit: contain; transition: transform .25s ease; } - html[data-theme="light"] .focus-image img { mix-blend-mode: multiply; } - .focus-card:hover .focus-image img { transform: scale(1.015); } - .focus-title { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 15px; } - .focus-title h2 { margin: 6px 0 3px; font-size: clamp(20px, 2vw, 27px); letter-spacing: -.025em; } - .focus-title h2, .focus-title strong { font-weight: 620; } - .focus-title p { margin: 0; color: var(--muted); font-size: 12px; } - .focus .status-chip { min-height: 0; padding: 0; background: transparent; color: var(--accent-soft); font-weight: 600; } - .focus-body { padding: 16px 0 20px; } - .focus-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } - .focus-stat { padding: 11px 11px 11px 0; background: transparent; } - .focus-stat + .focus-stat { padding-left: 11px; border-left: 1px solid var(--line); } - .focus-stat small, .focus-stat strong { display: block; } - .focus-stat small { color: var(--muted); font-size: 9px; } - .focus-stat strong { margin-top: 4px; font-size: 12px; font-weight: 600; } - .focus-card-actions { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin-top: 15px; } - .focus .focus-card-actions .button.primary { border: 1px solid var(--line-strong); background: transparent; color: var(--text); } - .today-panel { align-self: start; padding: 0 0 0 26px; border: 0; border-left: 1px solid var(--line); border-radius: 0; background: transparent; } - .today-panel h2 { margin: 0; font-size: 18px; } - .today-item { display: grid; grid-template-columns: 39px 1fr; gap: 11px; padding: 15px 0; border-bottom: 1px solid var(--line); } + .focus .side-nav { background: var(--surface); border-right: 1px solid var(--line); } + .focus .brand { border: 1px solid var(--line-strong); border-radius: 12px; background: var(--accent); color: var(--accent-ink); } + .focus .nav-link { border-radius: 12px; } + .focus .nav-link.active { background: var(--surface-3); color: var(--accent-soft); } + .focus-main { padding: 26px clamp(24px, 3.8vw, 64px) 120px; } + .focus .page-header h1 { font-size: clamp(28px, 2.8vw, 40px); font-weight: 750; letter-spacing: -.035em; } + .focus .button, .focus .icon-button, .focus .language { border-radius: 10px; } + .focus .language button { border-radius: 6px; } + .focus .header-controls .button.primary { border: 0; background: var(--accent); color: var(--accent-ink); font-weight: 720; } + + /* Zone badge styling */ + .zone-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 700; padding: 2.5px 8px; border-radius: 6px; line-height: 1.2; } + .zone-badge.good { background: rgba(52, 211, 153, 0.12); color: #34d399; border: 1px solid rgba(52, 211, 153, 0.28); } + .zone-badge.warn { background: rgba(251, 191, 36, 0.12); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.28); } + .zone-badge.bad { background: rgba(248, 113, 113, 0.12); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.28); } + .zone-badge.neutral { background: rgba(255, 255, 255, 0.06); color: var(--muted); border: 1px solid var(--line); } + .zone-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; } + + /* Range Gauge Bar */ + .range-gauge-wrap { display: flex; flex-direction: column; gap: 4px; width: 100%; margin-top: 5px; } + .range-gauge-track { position: relative; width: 100%; height: 5px; border-radius: 5px; background: rgba(255, 255, 255, 0.08); overflow: visible; } + html[data-theme="light"] .range-gauge-track { background: rgba(0, 0, 0, 0.08); } + .range-gauge-zone { position: absolute; top: 0; bottom: 0; background: linear-gradient(90deg, rgba(52, 211, 153, 0.35), rgba(74, 222, 128, 0.65)); border-radius: 3px; } + .range-gauge-needle { position: absolute; top: 50%; transform: translate(-50%, -50%); width: 10px; height: 10px; display: grid; place-items: center; z-index: 2; } + .range-needle-dot { width: 7px; height: 7px; border-radius: 50%; background: #ffffff; border: 1.5px solid #10b981; box-shadow: 0 0 5px rgba(255, 255, 255, 0.85); } + .range-gauge-labels { display: flex; justify-content: space-between; align-items: center; font-size: 9px; color: var(--muted); } + .range-target-label { color: #34d399; font-weight: 600; } + html[data-theme="light"] .range-target-label { color: #15803d; } + + /* Stage lifecycle progress bar */ + .stage-timeline { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin: 12px 0; } + .stage-step { display: flex; flex-direction: column; gap: 4px; } + .stage-step-bar { height: 4px; border-radius: 3px; background: var(--line-strong); } + .stage-step.done .stage-step-bar { background: rgba(74, 222, 128, 0.6); } + .stage-step.active .stage-step-bar { background: var(--accent); box-shadow: 0 0 8px var(--accent-glow); } + .stage-step small { font-size: 9.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .stage-step.active small { color: var(--text); font-weight: 700; } + + /* Modern Tent Sensor Cards */ + .modern-sensor-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin: 20px 0 28px; } + .sensor-card { background: var(--surface); border: 1px solid var(--line); border-radius: 16px; padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; transition: border-color .2s, transform .2s; } + .sensor-card:hover { border-color: var(--line-strong); transform: translateY(-1px); } + .sensor-card-head { display: flex; align-items: center; justify-content: space-between; } + .sensor-card-title { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 11.5px; font-weight: 600; } + .sensor-card-icon { width: 26px; height: 26px; border-radius: 7px; background: var(--surface-2); color: var(--accent-soft); display: grid; place-items: center; } + .sensor-card-val { font-size: 21px; font-weight: 750; letter-spacing: -.02em; color: var(--text); line-height: 1.1; margin: 2px 0 0; } + .sensor-card-sub { font-size: 10px; color: var(--muted); display: flex; justify-content: space-between; align-items: center; margin-top: 2px; } + + /* Modern Layout */ + .focus-plants { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 20px; width: 100%; } + + /* Modern Plant Cards */ + .modern-plant-card { background: var(--surface); border: 1px solid var(--line); border-radius: 20px; padding: 20px; display: flex; flex-direction: column; gap: 14px; box-shadow: var(--shadow); position: relative; transition: border-color .2s; } + .modern-plant-card:hover { border-color: var(--line-strong); } + .plant-card-header { display: grid; grid-template-columns: 116px 1fr; gap: 16px; align-items: start; } + .plant-thumb-wrap { position: relative; width: 116px; height: 116px; border-radius: 14px; overflow: hidden; background: var(--surface-2); } + .plant-thumb-wrap img { width: 100%; height: 100%; object-fit: cover; } + .plant-breeder-tag { position: absolute; bottom: 5px; left: 5px; right: 5px; padding: 2px 5px; border-radius: 5px; background: rgba(5, 12, 7, 0.85); backdrop-filter: blur(4px); color: #d1fae5; font-size: 9px; font-weight: 700; text-align: center; border: 1px solid rgba(255, 255, 255, 0.15); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .plant-info-block { display: flex; flex-direction: column; gap: 5px; min-width: 0; } + .plant-stage-badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 6px; background: rgba(52, 211, 153, 0.12); color: #34d399; border: 1px solid rgba(52, 211, 153, 0.28); font-size: 10.5px; font-weight: 750; width: fit-content; } + .pulse-dot { width: 6px; height: 6px; border-radius: 50%; background: #34d399; box-shadow: 0 0 6px #34d399; } + .plant-info-block h2 { margin: 0; font-size: 21px; font-weight: 750; letter-spacing: -.025em; color: var(--text); } + .plant-genetics-sub { color: var(--muted); font-size: 11.5px; margin: 0; line-height: 1.35; } + .plant-harvest-pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 6px; background: var(--surface-2); color: var(--muted); font-size: 10.5px; width: fit-content; margin-top: 2px; } + .plant-harvest-pill strong { color: var(--text); } + + /* Plant Telemetry 3-col Grid */ + .plant-telemetry-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 12px; background: var(--surface-2); border-radius: 12px; border: 1px solid var(--line); } + .telemetry-cell { display: flex; flex-direction: column; gap: 4px; } + .telemetry-cell-head { display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 10.5px; font-weight: 600; } + .telemetry-cell-val { font-size: 17px; font-weight: 750; color: var(--text); line-height: 1.1; } + + /* Card Bottom Action Bar */ + .plant-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; border-top: 1px solid var(--line); } + .plant-care-status { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 11px; } + .plant-card-actions { display: flex; align-items: center; gap: 8px; } + .plant-card-actions .button.primary { min-height: 34px; padding: 0 12px; font-size: 12px; } + .plant-card-actions .button.outline { min-height: 34px; padding: 0 10px; font-size: 12px; } + + .today-panel { align-self: start; padding: 18px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); } + .today-panel h2 { margin: 0 0 12px; font-size: 17px; font-weight: 700; } + .today-item { display: grid; grid-template-columns: 34px 1fr; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--line); } .today-item:last-of-type { border-bottom: 0; } - .today-icon { display: grid; width: 39px; height: 39px; place-items: center; border-radius: 0; background: transparent; color: var(--muted); } - .focus .today-item strong, .focus .today-panel h2 { font-weight: 620; } - .today-item strong, .today-item small { display: block; } - .today-item small { margin-top: 4px; color: var(--muted); line-height: 1.35; } + .today-icon { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 8px; background: var(--surface-2); color: var(--accent-soft); } + .today-item strong { font-size: 12px; font-weight: 650; display: block; } + .today-item small { margin-top: 2px; color: var(--muted); line-height: 1.3; display: block; font-size: 10.5px; } /* B. One selected plant becomes the workspace. */ .workspace { min-height: 100vh; background: var(--bg); } @@ -911,7 +954,48 @@ const createCopy={de:{createRuns:"Neue Pflanze anlegen",createIntro:"Eine Pflanze wird als eigener Lauf angelegt. Danach kannst du direkt die nächste Pflanze anlegen.",stepPlant:"Pflanze",stepSetup:"Phasen & Sensoren",stepReview:"Prüfen",chooseStrain:"Strain suchen",chooseStrainHint:"Suche wie in der bisherigen Version nach Breeder und Strain. Die Auswahl hilft beim ersten Erntekorridor und bleibt vollständig änderbar.",nickname:"Spitzname",nicknameOptional:"Optional",plantDate:"Pflanzdatum",breederDuration:"Breeder-Angabe",harvestEstimate:"Erster Erntekorridor",stages:"Phasen",customPhase:"Eigene Phase hinzufügen",setup:"Sensoren zuordnen",setupHint:"Nur Pflanzensensoren sind exklusiv. Zeltwerte dürfen gemeinsam genutzt werden.",soilSensor:"Bodensensor",sharedClimate:"Klima",lightSensor:"Helligkeit",doorSensor:"Zelttür",later:"Später zuordnen",sensorA:"Bodensensor A · Feuchte, Temperatur, EC",sensorB:"Bodensensor B · Feuchte, Temperatur, Luftfeuchte",averageSensors:"Mittelwert aus 3 Quellen",sharedTent:"Gemeinsamer Zeltwert",continue:"Weiter",back:"Zurück",review:"Lauf prüfen",createRun:"Pflanze anlegen",runCreated:"Pflanze im Prototyp angelegt",prototypeOnly:"Nur Prototyp · noch keine Daten werden gespeichert",plantSpecific:"Je Pflanze",selected:"Ausgewählt",notAssigned:"Noch nicht zugeordnet",createdNext:"Als Nächstes kannst du den Lauf öffnen und direkt den ersten Eintrag erfassen.",openPrototype:"Zur Pflanzenansicht",allEditable:"Jeder Wert bleibt auch nach dem Anlegen änderbar.",summary:"Zusammenfassung",runSingular:"1 Pflanze · 1 Lauf",strainQuery:"Strain",strainQueryPlaceholder:"z. B. Northern Lights Automatic",breederQuery:"Breeder",breederQueryPlaceholder:"z. B. Royal Queen Seeds",search:"Suchen",searchFirst:"Gib Breeder und Strain ein. Die spätere HA-Version verwendet dafür wieder die vorhandene SeedFinder-Suche.",sampleResults:"SeedFinder-Vorschau · Beispieldaten",noResults:"Keine passenden Beispieltreffer. Du kannst die Angaben trotzdem manuell erfassen.",manualEntry:"Strain manuell eingeben",manualActive:"Manuelle Angaben",manualName:"Strainname",manualBreeder:"Breeder",manualDuration:"Dauer ab Pflanzdatum",daysFrom:"von Tagen",daysTo:"bis Tage",imagePending:"Breeder-Bild wird bei der echten Suche geladen",providerContract:"Produktion: authentifizierte HA-Suche · Fallback blockiert das Anlegen nie",searchNeedsBoth:"Breeder und Strain werden für die Suche benötigt."},en:{createRuns:"Add new plant",createIntro:"Each plant is created as its own run. You can add the next plant immediately afterwards.",stepPlant:"Plant",stepSetup:"Stages & sensors",stepReview:"Review",chooseStrain:"Search strain",chooseStrainHint:"Search by breeder and strain as in the previous version. The selection helps set the initial harvest window and remains fully editable.",nickname:"Nickname",nicknameOptional:"Optional",plantDate:"Planting date",breederDuration:"Breeder estimate",harvestEstimate:"Initial harvest window",stages:"Stages",customPhase:"Add custom stage",setup:"Assign sensors",setupHint:"Only plant sensors are exclusive. Tent values may be shared.",soilSensor:"Soil sensor",sharedClimate:"Climate",lightSensor:"Illuminance",doorSensor:"Tent door",later:"Assign later",sensorA:"Soil sensor A · moisture, temperature, EC",sensorB:"Soil sensor B · moisture, temperature, humidity",averageSensors:"Average of 3 sources",sharedTent:"Shared tent value",continue:"Continue",back:"Back",review:"Review run",createRun:"Add plant",runCreated:"Plant created in the prototype",prototypeOnly:"Prototype only · no data is saved yet",plantSpecific:"Per plant",selected:"Selected",notAssigned:"Not assigned yet",createdNext:"Next, open the run and capture the first entry directly.",openPrototype:"Open plant view",allEditable:"Every value remains editable after creation.",summary:"Summary",runSingular:"1 plant · 1 run",strainQuery:"Strain",strainQueryPlaceholder:"e.g. Northern Lights Automatic",breederQuery:"Breeder",breederQueryPlaceholder:"e.g. Royal Queen Seeds",search:"Search",searchFirst:"Enter breeder and strain. The HA build will reuse the existing SeedFinder search.",sampleResults:"SeedFinder preview · sample data",noResults:"No matching sample results. You can still enter the details manually.",manualEntry:"Enter strain manually",manualActive:"Manual details",manualName:"Strain name",manualBreeder:"Breeder",manualDuration:"Duration from planting",daysFrom:"from days",daysTo:"to days",imagePending:"Breeder image loads through the real search",providerContract:"Production: authenticated HA search · fallback never blocks creation",searchNeedsBoth:"Breeder and strain are required for search."}}; const params=new URLSearchParams(location.search); const state={variant:params.get("variant")||"A",detailVariant:params.get("detail")||"A",createVariant:"A",createStep:Math.min(3,Math.max(1,Number(params.get("step"))||1)),view:params.get("view")||"overview",lang:params.get("lang")||"de",theme:params.get("theme")||"dark",selectedPlant:params.get("plant")||"tangerine",selectedAction:"freeText",cleanMetric:params.get("metric")||"moisture",modal:null,entries:[],createdRuns:[],customPhases:[],strainQuery:"",breederQuery:"",strainSearchSubmitted:false,manualStrain:false,createDraft:{strainId:params.get("strain")||"",nickname:"",planted:"2026-08-21",schedule:"18/6",soilSensor:"later",climate:"average",light:"shared",door:"shared",manualName:"",manualBreeder:"",manualLow:"",manualHigh:""}}; - const plants={tangerine:{id:"tangerine",name:"Tangerine Dream Auto",breeder:"Zamnesia",stage:"seedling",day:1,image:"./assets/tangerine-dream-breeder.jpg",moisture:"46 %",conductivity:"—",harvestDe:"29. Okt. – 5. Nov.",harvestEn:"Oct 29 – Nov 5"},diesel:{id:"diesel",name:"Diesel Auto",breeder:"Royal Queen Seeds",stage:"seedling",day:1,image:"./assets/diesel-auto-breeder.jpg",moisture:"52 %",conductivity:"0 µS/cm",harvestDe:"18. – 23. Nov.",harvestEn:"Nov 18 – 23"}}; + const plants={ + tangerine:{ + id:"tangerine", + name:"Tangerine Dream Auto", + breeder:"Zamnesia", + stage:"seedling", + day:5, + genetics:"Autoflower · Feminisiert", + container:"11L Stofftopf (BioBizz Light-Mix)", + image:"./assets/tangerine-dream-breeder.jpg", + moistureVal:58, + moisture:"58 %", + tempVal:23.8, + temp:"23,8 °C", + ecVal:1.2, + conductivity:"1,2 mS/cm", + harvestDe:"29. Okt. – 5. Nov.", + harvestEn:"Oct 29 – Nov 5", + harvestDaysLeft:65, + lastWatering:"Vor 18 Std. gegossen (350 ml)" + }, + diesel:{ + id:"diesel", + name:"Diesel Auto", + breeder:"Royal Queen Seeds", + stage:"seedling", + day:5, + genetics:"Autoflower · Feminisiert", + container:"11L Stofftopf (Plagron Promix)", + image:"./assets/diesel-auto-breeder.jpg", + moistureVal:52, + moisture:"52 %", + tempVal:24.1, + temp:"24,1 °C", + ecVal:1.1, + conductivity:"1,1 mS/cm", + harvestDe:"18. – 23. Nov.", + harvestEn:"Nov 18 – 23", + harvestDaysLeft:68, + lastWatering:"Vor 18 Std. gegossen (350 ml)" + } + }; const strainCatalog=[ {id:"tangerine",name:"Tangerine Dream Auto",breeder:"Zamnesia",duration:[70,77],image:"./assets/tangerine-dream-breeder.jpg"}, {id:"diesel",name:"Diesel Auto",breeder:"Royal Queen Seeds",duration:[90,95],image:"./assets/diesel-auto-breeder.jpg"}, @@ -931,11 +1015,198 @@ function sideNav(){return ``} function mobileNav(){return ``} function pageHeader(){return ``} - function sensorRibbon(){const s=[["temp",t("temperature"),"24,2 °C"],["drop",t("humidity"),"63,5 %"],["sun",t("light"),"138 lx"],["power",t("lamp"),t("on")],["door",t("door"),t("closed")],["chart",t("energy"),"489,03 kWh"]];return `
${s.map(([i,l,v])=>`
${icon(i,true)}${l}${v}
`).join("")}
`} + function renderRangeBar(value, min, max, targetLow, targetHigh, unit = "") { + const targetLowPct = Math.max(0, Math.min(100, ((targetLow - min) / (max - min)) * 100)); + const targetWidthPct = Math.max(2, Math.min(100 - targetLowPct, ((targetHigh - targetLow) / (max - min)) * 100)); + const currentPct = Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)); + return `
+
+
+
+ +
+
+
+ ${min}${unit} + Ziel ${targetLow}–${targetHigh}${unit} + ${max}${unit} +
+
`; + } + + function renderStatusBadge(value, targetLow, targetHigh, targetText) { + let status = "good"; + let label = targetText || "Optimal"; + if (value < targetLow) { + status = "warn"; + label = "Niedrig"; + } else if (value > targetHigh) { + status = "warn"; + label = "Erhöht"; + } + return `${label}`; + } + + function sensorRibbon() { + const cards = [ + { + icon: "temp", + label: t("temperature"), + val: "26,9 °C", + numeric: 26.9, + min: 18, + max: 34, + targetLow: 24, + targetHigh: 28, + unit: "°C", + badge: "Optimal", + sub: "Min 22,4 °C · Max 27,2 °C" + }, + { + icon: "drop", + label: t("humidity"), + val: "69,5 %", + numeric: 69.5, + min: 40, + max: 90, + targetLow: 65, + targetHigh: 75, + unit: "%", + badge: "Sämling · Ideal", + sub: "Min 61,0 % · Max 72,0 %" + }, + { + icon: "eye", + label: "VPD (Dampfdruck)", + val: "0,94 kPa", + numeric: 0.94, + min: 0.4, + max: 1.8, + targetLow: 0.8, + targetHigh: 1.1, + unit: " kPa", + badge: "Ideal", + sub: "Ideale Transpiration" + }, + { + icon: "sun", + label: "Licht & DLI", + val: "56,5 klx", + numeric: 56.5, + min: 0, + max: 80, + targetLow: 40, + targetHigh: 65, + unit: " klx", + badge: "18/6h An", + sub: "DLI 37,6 mol/m²d · 78%" + }, + { + icon: "chart", + label: t("energy") + " & Last", + val: "497,8 kWh", + numeric: 280, + min: 0, + max: 400, + targetLow: 200, + targetHigh: 320, + unit: " W", + badge: "Live", + sub: "280 W Gesamtlast (~0,84 €/d)" + } + ]; + + return `
+ ${cards.map(c => ` +
+
+ ${icon(c.icon, true)}${c.label} +
+
${c.val}
+ ${renderRangeBar(c.numeric, c.min, c.max, c.targetLow, c.targetHigh, c.unit)} +
${c.sub}
+
+ `).join("")} +
`; + } + function stageTrack(){return `
${["seedling","vegetative","flowering","harvested"].map((k,i)=>`${t(k)}`).join("")}
`} - function focusCard(p){return `
${t(p.stage)} · ${t("day")} ${p.day}

${p.name}

${p.breeder}

${t("moisture")}${p.moisture}${t("harvestWindow")}${harvestWindow(p)}${t("next")}${t("plantToday")}
${stageTrack()}
`} + + function focusCard(p) { + const stage = p.stage || "seedling"; + const day = p.day || 5; + const stageLabel = t(stage); + const moistureVal = p.moistureVal || 52; + const tempVal = p.tempVal || 24.1; + const ecVal = p.ecVal || 1.1; + const stages = [ + { key: "germination", label: "Keimung", state: "done" }, + { key: "seedling", label: "Sämling", state: "active" }, + { key: "vegetative", label: "Vegetativ", state: "future" }, + { key: "flowering", label: "Blüte", state: "future" }, + { key: "harvested", label: "Ernte", state: "future" } + ]; + + return `
+
+
+ ${p.name} + ${p.breeder} +
+
+
+ ${stageLabel} · ${t("day")} ${day} +
+

${p.name}

+

${p.breeder} · ${p.genetics || "Autoflower"} · ${p.container || "11L Topf"}

+ 📅 Erntefenster:${harvestWindow(p)}(in ~${p.harvestDaysLeft || 65} T.) +
+
+ +
+ ${stages.map(s => ` +
+
+ ${s.label} +
+ `).join("")} +
+ +
+
+
+ ${t("moisture")} +
+
${moistureVal} %
+ ${renderRangeBar(moistureVal, 20, 80, 45, 65, "%")} +
+
+
+ Substrat-Temp +
+
${tempVal.toFixed(1).replace(".", ",")} °C
+ ${renderRangeBar(tempVal, 15, 32, 20, 26, "°C")} +
+
+
+ Leitfähigkeit (EC) +
+
${ecVal.toFixed(1).replace(".", ",")} mS/cm
+ ${renderRangeBar(ecVal, 0.0, 2.4, 0.8, 1.4, " mS")} +
+
+ +
+ 💧 ${p.lastWatering || "Vor 18 Std. gegossen (350 ml)"} +
+ + +
+
+
`; + } function todayPanel(){return ``} - function renderA(){return `
${sideNav()}
${pageHeader()}${sensorRibbon()}
${Object.values(plants).map(focusCard).join("")}
${todayPanel()}
${mobileNav()}
`} + function renderA(){return `
${sideNav()}
${pageHeader()}${sensorRibbon()}
${Object.values(plants).map(focusCard).join("")}
${mobileNav()}
`} function facts(p){return `
${t("stage")}
${t(p.stage)} · ${t("day")} ${p.day}
${t("planted")}
20.08.2026
${t("harvestWindow")}
${harvestWindow(p)}
${t("schedule")}
18/6
${t("moisture")}
${p.moisture}
${t("conductivity")}
${p.conductivity}
`} function workspaceTop(){return `
${icon("leaf")}PlantRun
${headerControls()}
`} function renderB(){const p=selected(),o=other();return `
${workspaceTop()}

${t("selectedPlant")}

${p.name}

${p.breeder} · ${t("prototype")}

${p.name} ${t(${icon("leaf",true)}${t("breederImage")}
${t(p.stage)} · ${t("day")} ${p.day}

${p.name}

${t("harvestWindow")}: ${harvestWindow(p)}

${stageTrack()}
${sensorRibbon()}
${mobileNav()}
`} diff --git a/docs/screenshots/ha_panel_overview_dark.png b/docs/screenshots/ha_panel_overview_dark.png new file mode 100644 index 0000000..5397180 Binary files /dev/null and b/docs/screenshots/ha_panel_overview_dark.png differ diff --git a/docs/screenshots/ha_panel_overview_light.png b/docs/screenshots/ha_panel_overview_light.png new file mode 100644 index 0000000..99b5a79 Binary files /dev/null and b/docs/screenshots/ha_panel_overview_light.png differ diff --git a/tests/frontend_harness.html b/tests/frontend_harness.html index 2b36932..a30772e 100644 --- a/tests/frontend_harness.html +++ b/tests/frontend_harness.html @@ -9,8 +9,42 @@