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 web/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
dist
dist-demo
build
.vite
*.log
Expand Down
46 changes: 43 additions & 3 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ npm run dev

The application will be available at `http://localhost:3000`. API requests will be proxied to the backend at `http://localhost:8080`.

### Demo Mode

For development or demonstration purposes without a backend server, you can run the application in demo mode. This mode uses realistic mock data that simulates all backend functionality:

```bash
npm run dev:demo
```

In demo mode:
- No backend connection is required
- Realistic simulated data updates every 10 seconds
- Solar power, battery, grid, and device data reflect realistic daily patterns
- Electricity prices vary based on time of day
- MPC decisions are generated for the next 24 hours
- All UI components work exactly as they would with a real backend

## Production Build

Build the application for production:
Expand All @@ -46,6 +62,27 @@ npm run build

The built files will be in the `dist` directory, which the Go backend serves at the root path.

### Demo Build

Build a standalone demo version that doesn't require a backend:

```bash
npm run build:demo
```

The demo build will be in the `dist-demo` directory. You can preview it with:

```bash
npm run preview:demo
```

The demo build is useful for:
- Demonstrations and presentations
- UI/UX testing without backend setup
- Showcasing the application to stakeholders
- Development when backend is unavailable
- Static hosting scenarios

## Project Structure

```
Expand Down Expand Up @@ -115,10 +152,11 @@ The web application consumes the following API endpoints:
The Vite configuration includes:

- Development server on port 3000
- API proxy to backend on port 8080
- WebSocket proxy for real-time updates
- Production build output to `dist/`
- API proxy to backend on port 8080 (disabled in demo mode)
- WebSocket proxy for real-time updates (mock data in demo mode)
- Production build output to `dist/` (or `dist-demo/` for demo builds)
- Source maps enabled
- Conditional compilation for demo mode via `__DEMO_MODE__` flag

## Customization

Expand Down Expand Up @@ -211,6 +249,8 @@ Historical data can be visualized with charts showing:
- Enable React DevTools for component inspection
- Check console for WebSocket connection status
- Use network tab to debug API issues
- Use demo mode for frontend-only development
- Mock data generator can be customized in `src/utils/mockData.ts`

## License

Expand Down
5 changes: 4 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:demo": "vite --mode demo",
"build": "tsc && vite build",
"preview": "vite preview"
"build:demo": "tsc && vite build --mode demo",
"preview": "vite preview",
"preview:demo": "vite preview --outDir dist-demo"
},
"dependencies": {
"react": "^18.2.0",
Expand Down
21 changes: 21 additions & 0 deletions web/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ body {
border: 1px solid var(--color-warning);
}

.demo-info-trigger {
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
font-weight: 600;
font-size: 0.875rem;
background-color: rgba(37, 99, 235, 0.2);
color: var(--color-primary);
border: 1px solid var(--color-primary);
cursor: pointer;
transition: all 0.2s;
}

.demo-info-trigger:hover {
background-color: rgba(37, 99, 235, 0.3);
transform: scale(1.05);
}

.demo-info-trigger:active {
transform: scale(0.98);
}

.main {
flex: 1;
padding: 2rem;
Expand Down
37 changes: 37 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,27 @@ import { PowerDisplay } from "./components/PowerDisplay";
import { SolarInfo } from "./components/SolarInfo";
import { MPCDecisions } from "./components/MPCDecisions";
import { MetricsSummary } from "./components/MetricsSummary";
import { DemoInfo } from "./components/DemoInfo";
import { useWebSocket } from "./hooks/useWebSocket";
import { useState, useEffect } from "react";

// Check if we're in demo mode
const isDemoMode = typeof __DEMO_MODE__ !== 'undefined' && __DEMO_MODE__;

function App() {
const { health, status, loading, error, wsConnected } = useWebSocket();
const [showDemoInfo, setShowDemoInfo] = useState(false);

// Show demo info automatically on first load in demo mode
useEffect(() => {
if (isDemoMode) {
const hasSeenDemo = localStorage.getItem('ems-demo-info-seen');
if (!hasSeenDemo) {
setShowDemoInfo(true);
localStorage.setItem('ems-demo-info-seen', 'true');
}
}
}, []);

if (loading) {
return (
Expand Down Expand Up @@ -50,6 +67,22 @@ function App() {
Energy Management System
</h1>
<div className="status-badges">
{isDemoMode && (
<StatusBadge
isActive={true}
activeLabel="🎭 Demo Mode"
inactiveLabel=""
/>
)}
{isDemoMode && (
<button
className="demo-info-trigger"
onClick={() => setShowDemoInfo(true)}
title="Learn about Demo Mode"
>
ℹ️
</button>
)}
<StatusBadge
isActive={isHealthy}
activeLabel="✓ Healthy"
Expand Down Expand Up @@ -246,6 +279,10 @@ function App() {
available power
</p>
</footer>

{isDemoMode && showDemoInfo && (
<DemoInfo onClose={() => setShowDemoInfo(false)} />
)}
</div>
);
}
Expand Down
193 changes: 193 additions & 0 deletions web/src/components/DemoInfo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
.demo-info-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.75);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

.demo-info-card {
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 2rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
border: 2px solid var(--color-primary);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
position: relative;
animation: slideUp 0.3s ease-out;
}

@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

.demo-info-close {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: none;
color: var(--color-text-secondary);
font-size: 1.5rem;
cursor: pointer;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}

.demo-info-close:hover {
background-color: rgba(255, 255, 255, 0.1);
color: var(--color-text);
}

.demo-info-card h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 1rem;
border-bottom: 2px solid var(--color-primary);
padding-bottom: 0.75rem;
}

.demo-info-description {
font-size: 1rem;
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
line-height: 1.6;
}

.demo-info-section {
margin-bottom: 1.5rem;
}

.demo-info-section h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
}

.demo-info-section p {
font-size: 0.9375rem;
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: 0.5rem;
}

.demo-info-section ul {
list-style: none;
padding: 0;
margin: 0;
}

.demo-info-section li {
font-size: 0.9375rem;
color: var(--color-text-secondary);
padding: 0.5rem 0;
padding-left: 0.5rem;
line-height: 1.5;
border-left: 3px solid var(--color-primary);
margin-bottom: 0.5rem;
background-color: rgba(37, 99, 235, 0.05);
border-radius: 0 4px 4px 0;
}

.demo-info-note {
background-color: rgba(234, 88, 12, 0.1);
border: 1px solid var(--color-warning);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.875rem;
line-height: 1.6;
color: var(--color-text-secondary);
}

.demo-info-note strong {
color: var(--color-warning);
font-weight: 600;
}

.demo-info-note code {
background-color: rgba(0, 0, 0, 0.3);
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-family: "Courier New", monospace;
font-size: 0.8125rem;
color: var(--color-text);
}

.demo-info-button {
width: 100%;
background-color: var(--color-primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.demo-info-button:hover {
background-color: #1d4ed8;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}

.demo-info-button:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

@media (max-width: 768px) {
.demo-info-card {
padding: 1.5rem;
width: 95%;
}

.demo-info-card h2 {
font-size: 1.5rem;
}

.demo-info-section h3 {
font-size: 1rem;
}

.demo-info-section p,
.demo-info-section li {
font-size: 0.875rem;
}
}
Loading