Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,4 @@ svc/data/jeti_sim_output/
# Local temporary research repo / scratch workspace
/.tmp_luox/
*.cap

80 changes: 80 additions & 0 deletions web/src/AppHMI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ActiveControllersBar from "./components/ActiveControllersBar";
import { controlManager, type ControlSource } from "./utils/controlManager";
import { useToast } from "./utils/toast";
import LogsPanel from "./components/LogsPanel";
import { Link } from "react-router-dom";
import LiveGraph from "./components/LiveGraph";
import { type SensorInfo, type SensorReadingResponse } from "./api";
import {
Expand Down Expand Up @@ -236,6 +237,21 @@ export default function AppHMI() {
const [visibleSensorIds, setVisibleSensorIds] = useState<string[]>([]);
const sensorVisibilityUserSet = useRef(false);
const [targetRoutineId, setTargetRoutineId] = useState<string | null>(null);
const [docsDropdownOpen, setDocsDropdownOpen] = useState<boolean>(false);
const docsDropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (docsDropdownRef.current && !docsDropdownRef.current.contains(event.target as Node)) {
setDocsDropdownOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

const [spectralModal, setSpectralModal] = useState<{ sensorId: string; fixedTs?: number } | null>(null);

const connectedSensorList = sortSensorsForDisplay(getConnectedSensors(sensors, latestMetrics));
Expand Down Expand Up @@ -648,6 +664,43 @@ export default function AppHMI() {
>
Routines
</button>

<div className={`hmi-dropdown ${docsDropdownOpen ? "open" : ""}`} ref={docsDropdownRef}>
<Link to="/docs" style={{ textDecoration: "none" }}>
<button
className="hmi-manage-btn"
title="View Docs"
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
Docs
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor" className="hmi-dropdown-arrow">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
</Link>
<div className="hmi-dropdown-menu">
<Link to="/docs" className="hmi-dropdown-item" onClick={() => setDocsDropdownOpen(false)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
General Docs
</Link>
<Link to="/docs/sensors" target="_blank" rel="noopener noreferrer" className="hmi-dropdown-item" onClick={() => setDocsDropdownOpen(false)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12a7.5 7.5 0 0015 0m-15 0a7.5 7.5 0 1115 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077l1.41-.513m14.095-5.13l1.41-.513M5.106 17.785l1.15-.827m11.379-8.16l1.15-.827M8.14 21.27l.707-1.03m7.45-10.86l.707-1.03M12 3v1.5m0 15V21m-9-9h1.5m12 0H21" />
</svg>
Sensor Setup & Quickstart
</Link>
<Link to="/docs/routines" target="_blank" rel="noopener noreferrer" className="hmi-dropdown-item" onClick={() => setDocsDropdownOpen(false)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
Routine Developer Docs
</Link>
</div>
</div>


</div>
</div>
</header>
Expand All @@ -668,6 +721,33 @@ export default function AppHMI() {
</button>
</div>

{mainTab === "sensors" && (
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "16px", marginBottom: "-4px" }}>
<Link to="/docs/sensors" target="_blank" rel="noopener noreferrer">
<button
className="hmi-manage-btn"
style={{
display: "flex",
alignItems: "center",
gap: "8px",
background: "linear-gradient(135deg, rgba(30, 64, 175, 0.25) 0%, rgba(37, 99, 235, 0.2) 100%)",
border: "1px solid rgba(59, 130, 246, 0.4)",
color: "#93c5fd",
fontSize: "13px",
fontWeight: "600",
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.15)"
}}
title="View Sensor Connection & Setup Quickstart Guide"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" style={{ width: "16px", height: "16px" }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
Sensor Docs/Quickstart
</button>
</Link>
</div>
)}

{mainTab === "control" && (
<>
{/* group control card, same visual treatment as a room section */}
Expand Down
90 changes: 90 additions & 0 deletions web/src/components/Docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from "react";
import { Link } from "react-router-dom";

export default function Docs() {
return (
<>
{/* Header matches AppHMI.tsx with clean, placeholder-free status layout */}
<header className="hmi-header">
<div className="hmi-header-inner">
<div className="hmi-brand">
<div className="hmi-logo"></div>
<div className="hmi-brand-text">
<h1>Glazing Control System</h1>
<p>Electrochromic Panel Management</p>
</div>
</div>
<div className="hmi-status">
<Link to="/">
<button className="hmi-manage-btn" title="Back to Home">
Back to Home
</button>
</Link>
</div>
</div>
</header>

<main className="hmi-main docs-layout">
{/* Title Card */}
<div className="room-section">
<div className="room-header">
<h1 className="room-title">Documentation Page</h1>
</div>
<p style={{ color: "var(--hmi-text-muted)", margin: "8px 0 0 0", fontSize: "14px" }}>
General system overview, layout information, and operational guide
</p>
</div>

{/* Section 1 */}
<div className="room-section">
<h2 className="room-header">What is the purpose of each menu?</h2>
<p>
This HMI panel manages electrochromic glazing segments for solar control and visual comfort. Use the top navigation bar and side panel shortcuts to command individual zones, schedule daily tint routines, or monitor live environment measurements.
</p>
</div>

{/* Section 2 */}
<div className="room-section">
<h2 className="room-header">Nav Bar Info</h2>
<ol style={{ lineHeight: "1.6", paddingLeft: "20px", margin: 0 }}>
<li style={{ marginBottom: "12px" }}>
<strong>Sim vs. Real:</strong>
<br />
We have a simulator option and a real option, the simulator being the default. When in the trailer and connected to Wi-Fi, switch to real.
</li>
<li style={{ marginBottom: "12px" }}>
<strong>Logs:</strong>
<ul style={{ marginTop: "8px", paddingLeft: "20px" }}>
<li style={{ marginBottom: "6px" }}><strong>Audit Log:</strong> Lists every change so far in the levels of each panel or group. Sort by date range, panels/group, and filter by specific panel ID or title. Export as CSV to keep all audits listed in their visible order.</li>
<li><strong>Sensor Log:</strong> Tracks live and historical environment measurements (such as illuminance, solar irradiance, GPS status, colorimetry, and spectral data). You can view individual log entries in detail (including historical spectral graphs for JETI devices) and export all logged data as CSV.</li>
</ul>
</li>
<li>
<strong>Manage:</strong>
<ul style={{ marginTop: "8px", paddingLeft: "20px" }}>
<li style={{ marginBottom: "6px" }}><strong>Groups:</strong> Allows managing and configuring panel zones dynamically.</li>
<li><strong>Routines:</strong> Allows scripting routines to execute on specific intervals or schedules.</li>
</ul>
</li>
</ol>
</div>

{/* Section 3 */}
<div className="room-section">
<h2 className="room-header">Group Control</h2>
<p>
Group controls allow overriding target levels for entire facades or multiple sensors simultaneously. Dwell rules prevent rapid transitions between tint states, maximizing glass longevity.
</p>
</div>

{/* Section 4 */}
<div className="room-section">
<h2 className="room-header">Windows</h2>
<p>
Individual electrochromic panels are labeled by their sector code (e.g. <code>P01</code>, <code>P02</code>). Clicking any window tile opens the manual command dialog to override its tint level.
</p>
</div>
</main>
</>
);
}
39 changes: 23 additions & 16 deletions web/src/components/RoutineCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,22 +351,29 @@ export default function RoutineCodeEditor({ panels, groups, initialRoutineId }:
<div className="form-group" style={{ position: 'relative' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label>Routine Name</label>
<Link
to="/docs"
target="_blank"
style={{
fontSize: "12px",
fontWeight: 600,
color: "var(--btn-blue)",
textDecoration: "none",
padding: "2px 6px",
border: "1px solid var(--btn-blue)",
borderRadius: "4px"
}}
>
View Documentation ↗
</Link>
</div>
<Link to="/docs/routines" target="_blank" rel="noopener noreferrer">
<button
className="hmi-manage-btn"
style={{
display: "flex",
alignItems: "center",
gap: "6px",
background: "linear-gradient(135deg, rgba(30, 64, 175, 0.25) 0%, rgba(37, 99, 235, 0.2) 100%)",
border: "1px solid rgba(59, 130, 246, 0.4)",
color: "#93c5fd",
fontSize: "12px",
fontWeight: "600",
padding: "4px 8px",
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.15)"
}}
title="View Routine Developer Documentation"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" style={{ width: "14px", height: "14px" }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
Routine Docs
</button>
</Link> </div>
<input
type="text"
value={routineName}
Expand Down
Loading
Loading