Files
claudetools/clients/dataforth/power-monitor-demo/GATEWAY-SPEC.md
Mike Swanson 90e2cb2dd7 sync: auto-sync from GURU-5070 at 2026-06-05 08:06:47
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-05 08:06:47
2026-06-05 08:06:54 -07:00

258 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 56 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.*