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

14 KiB
Raw Blame History

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.

  • 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)

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.