v1.0.656 — Customer portal (portal.sentinelsignal.io) brand redesign
Context
The homepage rebrand (v1.0.654) gave sentinelsignal.io a new dark "Sentinel Signal Systems" identity. The user reported that portal.sentinelsignal.io/portal/status — and by extension the whole customer portal — still looked like the old product: a light cream theme, inconsistent per-page nav, and copy that predated the rebrand. Since the portal is directly, publicly reachable (not gated behind the marketing site), visitors landing there saw a jarring, disconnected brand. The user asked for a full redesign, not just a color swap.
What was found
app/main.pyhas no Jinja2/templating engine. 9 routes read a static file fromapp/ui/*.htmlverbatim and splice SEO<head>tags in via string.replace(). 4 routes (/portal/documents[/{slug}],/portal/blog[/{slug}]) build the entire page as a Python f-string directly inmain.py.- No shared header/footer mechanism existed at all. Every one of the 13
app/ui/*.htmlfiles hardcoded its own<head>, its own<header class="hero">(doubling as both hero and nav — there was no persistent navbar), and its own<footer>with a duplicated inline year-script. These had already drifted (different nav-link sets per page). app/ui/styles.css(810 lines) was only lightly tokenized — a handful of:rootvars plus ~34 hardcoded hex colors scattered through component rules. A pure token swap would not have fully re-themed the page.- Fonts already matched (Space Grotesk + IBM Plex Mono, same Google Fonts import as marketing) — just not applied consistently.
- Three pages carry real product functionality via ID/class-selector JavaScript, not just decoration:
console.html(live API console, 689-lineapp.js),portal-lite.html(copy-to-clipboard CLI snippet generation), andportal-lite-dashboard.html(1,014-line account-management mini-SPA — signup/login, trial-key issuance, Stripe checkout, API-key create/rotate/revoke against live control-plane/billing endpoints).
Design decision
Every element ID and CSS class selected by JavaScript stays on the exact same element, unrenamed. Everything about how those elements look — colors, fonts, spacing, borders, buttons, cards, tables, pills, badges — was fully rebuilt to the new design language, not just recolored. New chrome (navbar, footer, section dividers) was added structurally since none of it is JS-selected. This delivered the full redesign while keeping live billing/auth/console functionality byte-identical — confirmed live-testable via getElementById/querySelector checks against the real rendered response, not just visual inspection.
What shipped
Shared navbar + footer (app/main.py): two new render functions, _render_portal_navbar() and _render_portal_footer(), generate the new chrome once. _inject_portal_chrome() swaps <!--PORTAL_NAVBAR--> (inserted after <body>) and <!--PORTAL_FOOTER--> (replacing each page's old footer) markers for the rendered output, wired into the existing _portal_html_response() helper — so all 9 static-file routes and all 4 f-string routes get it automatically, no per-route changes needed beyond the marker swap in each page source. This fixed the actual drift bug: every page now shows the same nav links and the same footer (brand column with contact emails, Product/Developers/Company link columns, "SIGNAL BEFORE ACTION." tagline, legal line).
Full visual rebuild (app/ui/styles.css): new :root tokens matching the marketing palette (--bg: #0b0f14, --panel: #151e27, --signal: #e7b84b, --healthy/--warning/--critical). Every component rule that hardcoded a color was rebuilt: buttons, cards (.op-card, .docs-card, .plan-card), method pills (GET/POST/PUT/DELETE now map to healthy/signal/warning/critical), badges, code blocks, tables — all restyled to the new dark, amber-accented visual language. New shared-chrome classes (.site-header, .nav-row, .brand-mark, .nav-links, .site-footer, .footer-grid) duplicated from marketing's stylesheet (no shared-asset pipeline exists between the two separately-deployed services). The SignalLine motif (inline SVG + CSS pulse animation, prefers-reduced-motion-aware) was added as a section divider on the eight lower-risk, mostly-static pages (index, docs-home, pricing, governance, terms, privacy, mcp, mcp-demo) — skipped entirely on the three JS-heavy pages to keep their structural risk at zero.
Copy alignment (app/ui/index.html, /portal route): the portal's own hero eyebrow and page title changed from "Sentinel Signal Systems" (now redundant — the navbar carries that identity site-wide) to "Sentinel Signal Healthcare Intelligence" — this portal is that product's own app, the same relationship verify.sentinelsignal.io has to naming itself "Verify" under the parent brand. No other copy changed.
Verification
tests/unit/test_product_messaging.py: new tests assert the shared navbar/footer functions carry every required link/brand element, every one of the 13 static pages carries both chrome markers and no stale per-page footer, and the stylesheet uses the new design tokens.- Full suite run:
python -m pytest tests/ -q— 321 passed, zero failures, including 3 new tests added for this change. - Live-rendered verification via
TestClientagainst the real FastAPI routes (not just raw source files): all 15 portal routes return 200 with exactly one navbar/footer/year element each and no leftover unreplaced markers; every JS-dependent element ID on the three high-risk pages (console, portal-lite, portal-lite-dashboard — ~30 IDs checked) confirmed present and unchanged.
Not done / explicitly out of scope
- Verify's own UI (a third, separately-themed dark palette — teal/cyan accents, system fonts, no shared tokens with either marketing or the portal) was identified during investigation as a real brand inconsistency but left untouched — not part of this task.
- No change to portal page content/IA (docs, pricing, legal text, MCP quickstart) beyond the one
/portaleyebrow/title alignment above — visual restyling only, per the same boundary already established for the marketing-site rebrand. - No new templating engine or frontend framework/build step introduced — the marker-based chrome injection extends the existing string-replace pattern already used for SEO head injection.