258 lines
14 KiB
Markdown
258 lines
14 KiB
Markdown
# Power Monitor Demo — Gateway & Publishing Spec
|
||
|
||
**Status:** DRAFT v0.1 (living document) · **Date:** 2026-06-05
|
||
**Owner:** Arizona Computer Guru (advisory) · **Implementer:** Georg Haubner (Dataforth)
|
||
**Subject:** Standing up a gated, proxied demo of the Power Monitor UI at `PWM.dataforth.com`
|
||
|
||
> ACG's role here is to **shape the design via feedback**, not to modify the application
|
||
> code. This document is the reference architecture we hand to Georg. The SPA itself
|
||
> ("ExternalCodeReview" build) is unchanged except where called out under
|
||
> **Feedback for Georg**.
|
||
|
||
---
|
||
|
||
## 1. Purpose & Scope
|
||
|
||
Publish a demo of the Power Monitor dashboard so **technology partners and Dataforth
|
||
staff** can run feature-testing and feedback sessions. This is **not** the shipped
|
||
client product — it is a hosted, controlled preview.
|
||
|
||
In scope: hosting model, auth, the proxy/policy gateway, per-user permissioning,
|
||
and the consolidated code-review feedback. Out of scope: rebuilding the meter
|
||
firmware API or the SPA's internals.
|
||
|
||
**Delivery model:** the shipped product is a **computer-run application** that connects to
|
||
meters over the network and displays their data — it is **not** flashed onto the meter
|
||
(hardware limits). The demo is the hosted, browser-accessible version of that same app.
|
||
This corrects the "served from embedded flash" rationale in the source's own `project.md`
|
||
(see Feedback). Practically: the **shipped product = a local app on the customer's network**
|
||
(the hardened `dev_server.exe` lineage); the **demo = the hosted version of that app** behind
|
||
our gateway. Same proxy concept, two trust levels — which is why the SSRF/open-proxy concern
|
||
is **High for the public demo** but **much lower for the local product** (own LAN, own meters).
|
||
|
||
## 2. Architecture Overview
|
||
|
||
```
|
||
Browser ──HTTPS──> PWM.dataforth.com
|
||
│ (outbound tunnel; no meter is ever publicly exposed)
|
||
▼
|
||
┌──────────────────────────┐
|
||
│ Demo Gateway │ hosted INSIDE the Dataforth network
|
||
│ • serves static SPA │
|
||
│ • passwordless sessions │
|
||
│ • POLICY enforcement pt │
|
||
│ • proxies ONLY live mtrs │
|
||
└───────────┬──────────────┘
|
||
│ (allowlisted)
|
||
▼
|
||
Live DTF meters (LAN)
|
||
Simulated units = generated client-side in the browser; never touch the gateway.
|
||
```
|
||
|
||
Key insight from code review: **simulated units are produced client-side**; only
|
||
**live meters** generate network traffic (`/api/Data?ip=`). Therefore the gateway's
|
||
security boundary is precisely *"what may this user do to a real meter,"* and that
|
||
must be enforced **on the wire** at the gateway — never trusting the app's
|
||
(cosmetic) client-side button gating.
|
||
|
||
## 3. Device Classes (all three visible in the demo)
|
||
|
||
| Class | Source | Marker | Reaches gateway? |
|
||
|---|---|---|---|
|
||
| **Simulated** | baked into the build | **"SIMULATION" disclaimer** (see Feedback) | No — client-side only |
|
||
| **Live (curated)** | real DTF meters, fixed allowlist | live indicator | Yes — proxied |
|
||
| **Explorable** | user-entered IP within the meter subnet | — | Yes — internal users only |
|
||
|
||
The demo data Georg built into the executable **stays**. Live units **stay visible**
|
||
so staff can validate that real readings populate the dashboard correctly.
|
||
|
||
## 3.5 Demo Behavior & Build Approach (feedback for Georg)
|
||
|
||
### Show everything; simulate what the user can't really do
|
||
For a *feedback* demo, partners and staff must SEE the full feature set — firmware,
|
||
config, and IP screens included. So non-admins are **not** shown 403 errors and controls
|
||
are **not** hidden:
|
||
- Every screen and control renders for everyone.
|
||
- Capability held → the write is **real** (proxied to the meter).
|
||
- Capability absent → the write is routed to a **client-side simulation** returning a
|
||
believable result. Full interactive experience; no real meter touched.
|
||
|
||
(IP visibility for external users is the one exception — soft-hidden per the `see_ip`
|
||
nice-to-have.)
|
||
|
||
### Build it as an isolated DEMO MODULE — not a fork
|
||
The application is **actively evolving** and the point is to test *current* features.
|
||
- **Do NOT maintain a separate demo fork** — it drifts immediately; partners would test
|
||
stale features, and two codebases must be kept in sync. Wrong cost.
|
||
- **Do add a `demo` mode driven by an isolated module** (one scoped component, enabled by
|
||
a build flag / env). It applies the SIMULATION disclaimers, reads the user's
|
||
capabilities (gateway `/me` endpoint), and provides the write-simulation shim above.
|
||
The shipped product builds **without** the module, so none of it can activate in
|
||
production.
|
||
|
||
### A clean strip point for the production build (consideration)
|
||
Georg will eventually want a production build with **no demo/mock code** in it. Since this
|
||
is a **computer-run app, not meter firmware**, the motivation is **code hygiene + safety**
|
||
(not flash size): removing any path by which fabricated values could render on the shipped
|
||
product — the flip side of the "mock engine fused into the live data path" finding.
|
||
|
||
Two clean options, his call:
|
||
- **Stay buildless** (current style) -> draw the boundary at the **file level**: extract all
|
||
sim/mock/demo logic out of `w3js_main.js` into a standalone `demo_module.js`; the demo
|
||
`index.html` includes it, the production bundle omits it. A runtime `if (DEMO_MODE)` flag
|
||
is NOT a strip — the code still ships in the bundle.
|
||
- **Add a small build step** — now a real option, since the original "no Node on embedded
|
||
hardware" rationale no longer applies. A bundler can dead-code-eliminate a `DEMO_MODE`
|
||
flag at compile time.
|
||
|
||
Either way it's the same extraction that resolves the "simulation fused into the live path"
|
||
finding — do it once, serve both demo and product. Strictly necessary is Georg's judgment;
|
||
cheap now, costly to retrofit.
|
||
|
||
### Two layers, on purpose (defense in depth)
|
||
- **Demo module (in the app) = UX layer** — show-all, simulate writes, disclaimers.
|
||
Trusted only for presentation.
|
||
- **Gateway = hard security boundary** — independently blocks any real write a user isn't
|
||
entitled to, on the wire, regardless of what the browser does. Never trusts the app.
|
||
|
||
The two are independent: even with a bug in the demo module, the gateway still prevents a
|
||
non-privileged user from modifying a real meter.
|
||
|
||
## 4. Authentication — passwordless magic links
|
||
|
||
- Admin (internal) adds a user (name + permission checkboxes); the gateway mints a
|
||
**one-time magic link**. Admin delivers it however is convenient (email, Teams,
|
||
in person). Clicking it establishes a durable signed session cookie.
|
||
- **No passwords** are ever set, stored, rotated, or emailed.
|
||
- **Revoke** = set the user inactive; the session is invalidated.
|
||
- v1 needs **no email infrastructure** — the admin copies the link. Automated send
|
||
via Dataforth M365 SMTP can be added later without changing the model.
|
||
- **Session lifetime:** Georg's call (a sane default + optional auto-expiry for
|
||
external users after the feedback window).
|
||
|
||
## 5. Authorization — `internal` is the master gate
|
||
|
||
| | External user | Internal user |
|
||
|---|---|---|
|
||
| Simulated units | full (harmless) | full |
|
||
| Live meters | **read-only, IPs hidden** | read-only + (per checkbox) write |
|
||
| See / explore / add / modify IPs | **never** | yes (within meter subnet) |
|
||
| Destructive on real meters (config/reboot/firmware) | **never** | per-user checkbox |
|
||
| Manage user list (`is_admin`) | never | internal only (Georg + Mike) |
|
||
|
||
- **All IP-related capability is gated by `internal`** — external users never see,
|
||
explore, add, or modify anything IP-related.
|
||
- The per-user checkboxes (`write_config`, `reboot_restore`, `update_firmware`) are
|
||
finer control **among internal users**, applied to real meters.
|
||
- **Visibility != capability:** non-admins **see every option**; lacking a capability
|
||
means that action is **simulated**, not hidden or errored (see §3.5).
|
||
- **New-user defaults:** `internal=false`, `is_admin=false`, all checkboxes off →
|
||
read-only + simulated units only.
|
||
|
||
## 6. User Store (gateway-owned)
|
||
|
||
```yaml
|
||
user:
|
||
name: "Jane Partner"
|
||
login_handle: jane@partner.example # used to mint/deliver the magic link
|
||
internal: false # master gate for IP + destructive surface
|
||
is_admin: false # manage user list (internal only)
|
||
active: true # uncheck to revoke
|
||
capabilities: # only meaningful when internal=true
|
||
write_config: false
|
||
reboot_restore: false
|
||
update_firmware: false
|
||
# IP-related capability (see/explore/modify) is implied by internal=true, not a flag.
|
||
```
|
||
|
||
Flat file or SQLite the gateway owns. Managed via an internal-only `/admin` UI.
|
||
|
||
## 7. Gateway Enforcement
|
||
|
||
**Request flow:**
|
||
1. Browser → tunnel → gateway. No valid session → magic-link login.
|
||
2. Gateway serves the static SPA bundle.
|
||
3. App issues `/api…?ip=` or `/Control…`. Gateway intercepts every request:
|
||
a. Resolve user + capabilities from the session.
|
||
b. **Classify the target IP:** curated-live / explorable-subnet / out-of-range.
|
||
c. Apply policy (table below). Deny → `403 "not permitted in this demo."`
|
||
d. Allowed → proxy to the real meter; relay response.
|
||
|
||
**Policy (live-meter traffic only):**
|
||
|
||
| Action | Endpoints | Allowed for |
|
||
|---|---|---|
|
||
| Read telemetry/config | `GET /api/*`, `GET /Control` | everyone (curated-live); internal (explorable) |
|
||
| Target an explorable IP | any, with `?ip=` outside curated list | **internal only**, and **only within the meter subnet allowlist** |
|
||
| Write config | `PUT/POST /api/Config/*` | internal + `write_config` |
|
||
| Reboot / restore | `POST /Control/SystemRestart`, `/Control/RestoreToDefault` | internal + `reboot_restore` |
|
||
| Firmware update | `PUT /Control/FirmwareUpdate`, `POST /UpdateFile/*` | internal + `update_firmware` |
|
||
|
||
**SSRF prevention:** the gateway proxies **only** to IPs inside a configured **meter
|
||
subnet allowlist** — never arbitrary hosts. This structurally eliminates the open-proxy
|
||
behavior in the current `dev_server.py`. External users are further restricted to the
|
||
**curated** live list (a subset of the allowlist).
|
||
|
||
## 8. Hosting & Network
|
||
|
||
- **Host the gateway INSIDE the Dataforth network** (small VM/container on the meter
|
||
VLAN) so it can reach live meters directly.
|
||
- **Expose only the gateway** publicly via an **outbound tunnel** (Cloudflare Tunnel
|
||
or ACG NPM) → `PWM.dataforth.com`. No meter is ever directly reachable from the
|
||
internet. This is the correct answer to "need an internal IP available for an
|
||
external connection" — publish the *gateway*, not a meter IP.
|
||
- **TLS:** terminated at the tunnel/edge (Cloudflare or Let's Encrypt).
|
||
- **DNS:** `PWM.dataforth.com` → edge → tunnel → gateway.
|
||
|
||
## 9. Build / Publish
|
||
|
||
The SPA is static; "build" = produce a clean bundle:
|
||
- Remove dev/forensic artifacts: `search_transcript_*.py`, `query_all_devices.py`,
|
||
and the `original_ui/` duplicate tree (also leaks internal meter IPs + a developer
|
||
username — must not ship externally).
|
||
- Pin CDN dependencies (jQuery, Chart.js) **locally** for reliability and offline
|
||
operation (a customer-site app shouldn't depend on public CDNs).
|
||
- Gateway serves the bundle; live data flows through the policy proxy.
|
||
|
||
## 10. Feedback for Georg (consolidated from the code review)
|
||
|
||
**A. Demo-publish items**
|
||
1. **Simulation disclaimer** — mark simulated units with a clear "SIMULATION —
|
||
representative of real product data" badge. (Keep the demo data; just label it.)
|
||
2. **No open proxy** — the public demo must use the allowlisted gateway, never the
|
||
current `dev_server.py` (binds all interfaces, proxies arbitrary `ip=`).
|
||
3. **Strip dev/forensic artifacts** before any external handoff (see §9).
|
||
4. **Enforce permissions at the gateway**, not via client-side button-disabling
|
||
(which is cosmetic and bypassable).
|
||
|
||
**B. Product-ship items (for the eventual client product, not the demo)**
|
||
- On-device **TLS** (login/creds/SMTP currently traverse plain HTTP).
|
||
- **Firmware image signature verification** on the meter (the UI validates nothing).
|
||
- Confirm `RestoreToDefault` is server-authorized (callable without a session client-side).
|
||
- **Escape device-supplied strings** before `innerHTML` (stored DOM-XSS via TagName/name).
|
||
- Replace the **numeric session token** with an opaque/expiring token; add idle timeout.
|
||
- De-duplicate the hardcoded IP allowlists (copied across 5–6 files) and collapse the
|
||
`original_ui/` + `_v2` duplicate trees.
|
||
- Separate the client-side mock engine from the live data path so real meters never
|
||
show fabricated values.
|
||
|
||
## 11. Open Parameters (need Georg / Dataforth input)
|
||
|
||
1. **Curated live-meter allowlist** — which real meters are visible in the demo
|
||
(names + IPs), and which are external-visible vs internal-only.
|
||
2. **Meter subnet allowlist** — the IP range `explore_ip` (internal) may target.
|
||
3. **Session lifetime** — Georg's call (+ external auto-expiry?).
|
||
4. **Magic-link delivery** — manual copy for v1, or wire M365 SMTP now?
|
||
5. **Hosting specifics** — VM/container location on the DTF network; tunnel choice
|
||
(Cloudflare Tunnel vs ACG NPM).
|
||
|
||
## 12. Future / Out of Scope (noted, not required for the demo)
|
||
|
||
- **Opaque device handles** (gateway issues IDs; maps ID→IP) for *true* IP hiding
|
||
from external users — deferred; v1 uses simple UI-hiding (`see_ip` nice-to-have).
|
||
- On-device TLS, firmware signing, full session hardening — product-phase.
|
||
|
||
---
|
||
|
||
*Living draft — iterating with Mike before handoff to Georg.*
|