Remove edge glow; prefill MCMS URL from env; drop self-signed-cert option
- Remove the animated neon edge-glow entirely (HTML element + CSS) — it couldn't be made to follow the border reliably on iOS Safari. - Add PFW_MCMS_URL: the web server prefills the login "Host URL" field with it (injected as the input's value in the served index; still editable). Documented in env.example + web README. - Remove the "Accept self-signed TLS certificate" option: the checkbox is gone and both backends now always use rejectUnauthorized:true — MCMS must present a valid certificate. Dropped the acceptSelfSigned plumbing from the renderer and both login handlers. Docs: CLAUDE.md (drop edge-glow section, update login note) + web README. Verified: node --check; no stale refs (in-self-signed / acceptSelfSigned / edge-glow / beamspin); served index carries value="…" only when PFW_MCMS_URL is set, and no self-signed checkbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6f4a315947
commit
ea251c3498
8 changed files with 30 additions and 79 deletions
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -143,8 +143,10 @@ stream progress events back to the renderer.
|
||||||
|
|
||||||
### Fleet view
|
### Fleet view
|
||||||
|
|
||||||
- Login with email / password, accept-self-signed-TLS toggle (essential
|
- Login with email / password. TLS to MCMS is always verified
|
||||||
for lab MCMS installs).
|
(`rejectUnauthorized: true`); there is no accept-self-signed option —
|
||||||
|
MCMS must present a valid certificate. The web server can prefill the host
|
||||||
|
field from `PFW_MCMS_URL`.
|
||||||
- Bulk fleet load: `listAllOnuConfigs` + `listAllOnuStates` +
|
- Bulk fleet load: `listAllOnuConfigs` + `listAllOnuStates` +
|
||||||
`listAllOltStates`, all paginated via the `next` cursor at page size
|
`listAllOltStates`, all paginated via the `next` cursor at page size
|
||||||
100 (see §5).
|
100 (see §5).
|
||||||
|
|
@ -777,17 +779,9 @@ The ONU-state rows drill down: clicking one sets `#filter-status` and jumps
|
||||||
to the ONU tab (loading the fleet if needed). `fmtBps` in `app.js` is a
|
to the ONU tab (loading the fleet if needed). `fmtBps` in `app.js` is a
|
||||||
port of the iOS `Format.bps`.
|
port of the iOS `Format.bps`.
|
||||||
|
|
||||||
### Animated edge glow
|
(An animated neon edge-glow beam was tried and removed — it couldn't be made
|
||||||
|
to track the border reliably on iOS Safari. The static scanline overlay
|
||||||
`#edge-glow` (fixed, pointer-events-none, full-viewport) holds a `.beam`
|
remains.)
|
||||||
child masked to a 3px border ring (`mask` + `mask-composite: exclude`); the
|
|
||||||
ring contains a `::before` that is an oversized `conic-gradient` with a
|
|
||||||
white-hot core, spun with **`transform: rotate` 0→360°**, so the beam travels
|
|
||||||
around the screen edge. (It deliberately does *not* use an `@property`-
|
|
||||||
animated gradient angle — iOS Safari won't animate that, which left the beam
|
|
||||||
static on mobile.) The glow halo is a `drop-shadow` on the parent so it
|
|
||||||
survives the child's mask. Respects `prefers-reduced-motion`; z-index sits
|
|
||||||
below the scanline overlay (9999) and modals (10000).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
5
main.js
5
main.js
|
|
@ -63,11 +63,12 @@ function toWireError(err) {
|
||||||
return { message: err?.message || String(err) };
|
return { message: err?.message || String(err) };
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('api:login', async (_ev, { baseUrl, username, password, acceptSelfSigned }) => {
|
ipcMain.handle('api:login', async (_ev, { baseUrl, username, password }) => {
|
||||||
try {
|
try {
|
||||||
client = new McmsClient({
|
client = new McmsClient({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
rejectUnauthorized: !acceptSelfSigned,
|
// TLS is always verified — MCMS must present a valid certificate.
|
||||||
|
rejectUnauthorized: true,
|
||||||
// Verbose logging goes to the Electron terminal (npm start output).
|
// Verbose logging goes to the Electron terminal (npm start output).
|
||||||
// Turn off by setting PON_FLEET_VERBOSE=0 in the env.
|
// Turn off by setting PON_FLEET_VERBOSE=0 in the env.
|
||||||
verbose: process.env.PON_FLEET_VERBOSE !== '0',
|
verbose: process.env.PON_FLEET_VERBOSE !== '0',
|
||||||
|
|
|
||||||
|
|
@ -416,57 +416,6 @@ button.danger:hover:not(:disabled) {
|
||||||
.modal-type-label { display: block; }
|
.modal-type-label { display: block; }
|
||||||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 2px; }
|
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 2px; }
|
||||||
|
|
||||||
/* -------- Animated neon edge glow (beam travels around the border) --------
|
|
||||||
A rotating conic-gradient (transform: rotate — works on iOS Safari, unlike
|
|
||||||
an @property-animated gradient angle) clipped to a thin border ring. The
|
|
||||||
glow halo is a drop-shadow on the parent so it survives the child's mask. */
|
|
||||||
#edge-glow {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 9998;
|
|
||||||
pointer-events: none;
|
|
||||||
filter: drop-shadow(0 0 6px var(--neon)) drop-shadow(0 0 16px var(--cyan));
|
|
||||||
}
|
|
||||||
#edge-glow .beam {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
padding: 3px; /* thickness of the glowing border band */
|
|
||||||
overflow: hidden;
|
|
||||||
/* Keep only the padding ring (border band); punch out the interior. */
|
|
||||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
||||||
mask-composite: exclude;
|
|
||||||
}
|
|
||||||
#edge-glow .beam::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 200vmax; /* > viewport diagonal at any rotation */
|
|
||||||
height: 200vmax;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: conic-gradient(
|
|
||||||
from 0deg,
|
|
||||||
transparent 0deg,
|
|
||||||
transparent 40deg,
|
|
||||||
var(--cyan) 66deg,
|
|
||||||
#ffffff 90deg,
|
|
||||||
var(--neon) 114deg,
|
|
||||||
transparent 140deg,
|
|
||||||
transparent 360deg
|
|
||||||
);
|
|
||||||
animation: beamspin 7s linear infinite;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
@keyframes beamspin {
|
|
||||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
|
||||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
||||||
}
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
#edge-glow .beam::before { animation: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------- Top tabs -------- */
|
/* -------- Top tabs -------- */
|
||||||
.tabs { display: flex; gap: 4px; margin-left: 18px; align-self: stretch; align-items: flex-end; }
|
.tabs { display: flex; gap: 4px; margin-left: 18px; align-self: stretch; align-items: flex-end; }
|
||||||
.tabs .tab {
|
.tabs .tab {
|
||||||
|
|
|
||||||
|
|
@ -223,14 +223,13 @@ $('#btn-login').addEventListener('click', async () => {
|
||||||
const baseUrl = $('#in-host').value.trim();
|
const baseUrl = $('#in-host').value.trim();
|
||||||
const username = $('#in-user').value.trim();
|
const username = $('#in-user').value.trim();
|
||||||
const password = $('#in-pass').value;
|
const password = $('#in-pass').value;
|
||||||
const acceptSelfSigned = $('#in-self-signed').checked;
|
|
||||||
if (!baseUrl || !username || !password) {
|
if (!baseUrl || !username || !password) {
|
||||||
$('#login-error').textContent = 'Host, username and password are required.';
|
$('#login-error').textContent = 'Host, username and password are required.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$('#btn-login').disabled = true;
|
$('#btn-login').disabled = true;
|
||||||
$('#btn-login').textContent = 'Connecting…';
|
$('#btn-login').textContent = 'Connecting…';
|
||||||
const res = await window.api.login({ baseUrl, username, password, acceptSelfSigned });
|
const res = await window.api.login({ baseUrl, username, password });
|
||||||
$('#btn-login').disabled = false;
|
$('#btn-login').disabled = false;
|
||||||
$('#btn-login').textContent = 'Connect';
|
$('#btn-login').textContent = 'Connect';
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@
|
||||||
<link rel="stylesheet" href="app.css" />
|
<link rel="stylesheet" href="app.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Animated neon beam that travels around the screen edge. -->
|
|
||||||
<div id="edge-glow" aria-hidden="true"><div class="beam"></div></div>
|
|
||||||
|
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="brand">PON Fleet Upgrader</div>
|
<div class="brand">PON Fleet Upgrader</div>
|
||||||
<nav id="tabs" class="tabs hidden">
|
<nav id="tabs" class="tabs hidden">
|
||||||
|
|
@ -32,10 +29,6 @@
|
||||||
<label>Host URL<input id="in-host" type="url" placeholder="https://mcms.example.net" /></label>
|
<label>Host URL<input id="in-host" type="url" placeholder="https://mcms.example.net" /></label>
|
||||||
<label>Email / username<input id="in-user" type="text" autocomplete="username" /></label>
|
<label>Email / username<input id="in-user" type="text" autocomplete="username" /></label>
|
||||||
<label>Password<input id="in-pass" type="password" autocomplete="current-password" /></label>
|
<label>Password<input id="in-pass" type="password" autocomplete="current-password" /></label>
|
||||||
<label class="inline">
|
|
||||||
<input id="in-self-signed" type="checkbox" checked />
|
|
||||||
Accept self-signed TLS certificate
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="btn-login" class="primary">Connect</button>
|
<button id="btn-login" class="primary">Connect</button>
|
||||||
<span id="login-error" class="error"></span>
|
<span id="login-error" class="error"></span>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ cookie jar) per session.
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PORT` | `8080` | listen port |
|
| `PORT` | `8080` | listen port |
|
||||||
| `HOST` | `127.0.0.1` | bind address — `0.0.0.0` if the proxy is on another host |
|
| `HOST` | `127.0.0.1` | bind address — `0.0.0.0` if the proxy is on another host |
|
||||||
|
| `PFW_MCMS_URL` | — | prefill the login "Host URL" field (still editable) |
|
||||||
| `PFW_CACHE_TTL_SECONDS` | `60` | shared upstream cache TTL; `0` disables (see Caching) |
|
| `PFW_CACHE_TTL_SECONDS` | `60` | shared upstream cache TTL; `0` disables (see Caching) |
|
||||||
| `PFW_IDLE_MINUTES` | `30` | idle-session expiry (logs the MCMS session out) |
|
| `PFW_IDLE_MINUTES` | `30` | idle-session expiry (logs the MCMS session out) |
|
||||||
| `PFW_SECURE_COOKIE` | off | force the `Secure` cookie flag (else inferred from `X-Forwarded-Proto: https`) |
|
| `PFW_SECURE_COOKIE` | off | force the `Secure` cookie flag (else inferred from `X-Forwarded-Proto: https`) |
|
||||||
|
|
@ -142,8 +143,9 @@ LXC firewall + the NPM access list.
|
||||||
memory**. Run it behind HTTPS, keep `HOST=127.0.0.1`, and rely on the
|
memory**. Run it behind HTTPS, keep `HOST=127.0.0.1`, and rely on the
|
||||||
idle timeout. The session cookie is `HttpOnly`, `SameSite=Lax`, and
|
idle timeout. The session cookie is `HttpOnly`, `SameSite=Lax`, and
|
||||||
`Secure` behind TLS.
|
`Secure` behind TLS.
|
||||||
- "Accept self-signed TLS certificate" disables verification **to MCMS**
|
- TLS to MCMS is **always verified** — MCMS must present a valid certificate
|
||||||
for that session only (same as the desktop app).
|
(there is no accept-self-signed option). If your MCMS uses an internal CA,
|
||||||
|
install that CA on the host running the server.
|
||||||
- There's no app-level user directory — anyone who can reach the page and
|
- There's no app-level user directory — anyone who can reach the page and
|
||||||
has valid MCMS credentials can log in. Restrict network access at the
|
has valid MCMS credentials can log in. Restrict network access at the
|
||||||
proxy (mTLS / SSO / IP allow-list) if you need more than MCMS auth.
|
proxy (mTLS / SSO / IP allow-list) if you need more than MCMS auth.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
# Copy to /etc/pon-fleet-web.env and edit. All values are optional; the
|
# Copy to /etc/pon-fleet-web.env and edit. All values are optional; the
|
||||||
# defaults shown are what the server uses if a line is omitted.
|
# defaults shown are what the server uses if a line is omitted.
|
||||||
|
|
||||||
|
# Prefill the login "Host URL" field so operators don't type it each time.
|
||||||
|
# (Still editable in the form.) Use the MCMS PON Manager root URL.
|
||||||
|
#PFW_MCMS_URL=https://mcms.example.net
|
||||||
|
|
||||||
# Listen port.
|
# Listen port.
|
||||||
#PORT=8080
|
#PORT=8080
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const PORT = Number(process.env.PORT) || 8080;
|
||||||
const HOST = process.env.HOST || '127.0.0.1';
|
const HOST = process.env.HOST || '127.0.0.1';
|
||||||
const IDLE_MS = (Number(process.env.PFW_IDLE_MINUTES) || 30) * 60 * 1000;
|
const IDLE_MS = (Number(process.env.PFW_IDLE_MINUTES) || 30) * 60 * 1000;
|
||||||
const VERBOSE = process.env.PFW_VERBOSE === '1';
|
const VERBOSE = process.env.PFW_VERBOSE === '1';
|
||||||
|
const MCMS_URL = process.env.PFW_MCMS_URL || ''; // prefilled into the login host field
|
||||||
const CACHE_TTL_MS = (process.env.PFW_CACHE_TTL_SECONDS !== undefined
|
const CACHE_TTL_MS = (process.env.PFW_CACHE_TTL_SECONDS !== undefined
|
||||||
? Number(process.env.PFW_CACHE_TTL_SECONDS) : 60) * 1000;
|
? Number(process.env.PFW_CACHE_TTL_SECONDS) : 60) * 1000;
|
||||||
const ROOT = __dirname;
|
const ROOT = __dirname;
|
||||||
|
|
@ -208,6 +209,12 @@ function serveIndex(res) {
|
||||||
html = html.replace(/<script src="app\.js"><\/script>/i,
|
html = html.replace(/<script src="app\.js"><\/script>/i,
|
||||||
'<script src="/api-web.js"></script>\n <script src="/shared/app.js"></script>');
|
'<script src="/api-web.js"></script>\n <script src="/shared/app.js"></script>');
|
||||||
|
|
||||||
|
// Prefill the login host field from PFW_MCMS_URL (server-side default).
|
||||||
|
if (MCMS_URL) {
|
||||||
|
const v = MCMS_URL.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
html = html.replace(/(<input id="in-host"[^>]*?)\s*\/?>/i, `$1 value="${v}" />`);
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': MIME['.html'] });
|
res.writeHead(200, { 'Content-Type': MIME['.html'] });
|
||||||
res.end(html);
|
res.end(html);
|
||||||
}
|
}
|
||||||
|
|
@ -527,8 +534,9 @@ const server = http.createServer(async (req, res) => {
|
||||||
// Login establishes a session + its own McmsClient.
|
// Login establishes a session + its own McmsClient.
|
||||||
if (name === 'login') {
|
if (name === 'login') {
|
||||||
const body = await readBody(req, SMALL_BODY);
|
const body = await readBody(req, SMALL_BODY);
|
||||||
const { baseUrl, username, password, acceptSelfSigned } = body || {};
|
const { baseUrl, username, password } = body || {};
|
||||||
const client = new McmsClient({ baseUrl, rejectUnauthorized: !acceptSelfSigned, verbose: VERBOSE });
|
// TLS is always verified — MCMS must present a valid certificate.
|
||||||
|
const client = new McmsClient({ baseUrl, rejectUnauthorized: true, verbose: VERBOSE });
|
||||||
try {
|
try {
|
||||||
const result = await client.login(username, password);
|
const result = await client.login(username, password);
|
||||||
const sid = crypto.randomUUID();
|
const sid = crypto.randomUUID();
|
||||||
|
|
@ -590,5 +598,6 @@ server.listen(PORT, HOST, () => {
|
||||||
console.log(`[pon-fleet-web] listening on http://${HOST}:${PORT}`);
|
console.log(`[pon-fleet-web] listening on http://${HOST}:${PORT}`);
|
||||||
console.log(`[pon-fleet-web] idle session timeout: ${IDLE_MS / 60000} min · verbose MCMS: ${VERBOSE}`);
|
console.log(`[pon-fleet-web] idle session timeout: ${IDLE_MS / 60000} min · verbose MCMS: ${VERBOSE}`);
|
||||||
console.log(`[pon-fleet-web] upstream cache: ${cache.enabled() ? (CACHE_TTL_MS / 1000) + 's TTL (shared per MCMS host)' : 'disabled'}`);
|
console.log(`[pon-fleet-web] upstream cache: ${cache.enabled() ? (CACHE_TTL_MS / 1000) + 's TTL (shared per MCMS host)' : 'disabled'}`);
|
||||||
|
if (MCMS_URL) console.log(`[pon-fleet-web] login host prefilled with: ${MCMS_URL}`);
|
||||||
console.log('[pon-fleet-web] put a TLS-terminating reverse proxy (Caddy/nginx/NPM) in front for remote use.');
|
console.log('[pon-fleet-web] put a TLS-terminating reverse proxy (Caddy/nginx/NPM) in front for remote use.');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue