--- name: portal-ui-mockups description: "Build HTML mockups for IT Pro Partner operations portal — admin/customer dual-view, service integrations page, customer onboarding, router onboarding, router detail, network dashboard, DNS/domain management, login page, and event-specific forms." version: 1.10.0 author: ShoNuff platforms: [linux] metadata: hermes: tags: [portal, mockup, ui, rbac, html, tailwind, onboarding, mikrotik, dns, domain, login] --- # Portal UI Mockups Build single-file HTML mockups for the IT Pro Partner operations portal. All mockups are throwaway by design. ## Multi-role / RBAC mockups When the user wants both admin and customer views, build one file with a toggle button. Admin-only elements get `class="admin-only"`, customer-only get `class="customer-only"`. The toggle applies `role-customer` to the root, hiding admin-only content via `display: none`. ### Sidebar by role | Link | admin-only? | |---|---| | Dashboard | No (content differs) | | Servers | Yes | | Network Devices | Yes | | DNS | Yes | | Backups | Yes | | Call Logs | No (customer sees own calls) | | Reports | Yes | | Customers | Yes | Toggle button in sidebar footer switches role label, user name, and badge. ## Dashboard pages | Page | Admin sees | Customer sees | |---|---|---| | Dashboard | Server grid, activity feed, stats | My Services, recent calls | | Servers | Full server cards | Hidden | | Network | Full device table | Hidden | | DNS | Zone list with editing | Hidden | | Backups | Status table, restore | Hidden | | Call Logs | All customers filter | Own calls only | | Reports | 4 report types | Hidden | | Customers | Full table with billing | Hidden | ## Customer Onboarding Flow Three-step flow in one mockup file: 1. **Enter domain** — search input + "create blank customer" link 2. **Review matches** — auto-detected services from Cloudflare, RingLogix, UniFi, UISP, MXroute, RunCloud. Each shown with checkbox + platform badge + detail line. Not-found services shown with dashed borders and "Link manually" action 3. **Link & create** — batch action to link selected services and create customer record ### Status badges - Existing customer: bg-blue-100 text-blue-700 - DNS (Cloudflare): bg-amber-100 text-amber-700 - VoIP (RingLogix): bg-purple-100 text-purple-700 - Network (UniFi): bg-green-100 text-green-700 - WISP (UISP/CCR): bg-cyan-100 text-cyan-700 ## Login Page (login.html) Two-panel layout for unauthenticated visitors: | Left panel (desktop only) | Right panel | |---|---| | Blue gradient background with brand mark | Existing Customer login form | | Feature list (SSO, monitoring, RBAC) | Email + password fields | | Minimal "IT" logo + tagline | Remember me + forgot password | | | **OR** New Customer inquiry form | **Two login paths side-by-side:** - **Existing Customer (left card):** Blue accent. Email/password fields. Google SSO button. Forgot password link. - **New Customer (right card):** Green accent. First name + last name (split grid), email, company fields. Service interest checkboxes in 2-column grid (Internet/WISP, Business VoIP, DNS/Domain, Email Hosting, Web Hosting, Network Setup, Backup/DR, Other). Cloudflare Turnstile placeholder. "Submit Inquiry" button with 1 business day response note. Turnstile anti-bot placeholder: shield checkmark icon + "Protected by Cloudflare Turnstile" text. Invisible user experience when live. The login page is the landing for BOTH paths — existing customers sign in, new customers submit a lead form. This replaces a traditional single-form login page. Navigation links in top bar: Services, DNS (links to dns.html), Contact. ## DNS & Domains Management (dns.html) Two-tab layout. Tab switching via JS — add `class="tab-pane"` to each content div, and use `document.querySelectorAll('.tab-pane')` in the click handler. The tab handler selects panes by DOM index, not by closest parent selector, to avoid depth-traversal bugs. ### Tab 1: DNS Zones - Summary cards: Total Zones, Registered Domains, Expiring Soon (≤90 days), DNS Records - Zone list table: Zone name, Status badge, Plan, Record count, Linked Customer, Actions - DNS Record Editor (below zone list): A, AAAA, CNAME, MX, TXT, SRV records. Type badge colors: A=blue, CNAME=green, MX=purple, TXT=cyan, SRV=orange, AAAA=green. Edit/Del actions per record. "Back to zones" breadcrumb. "Add Record" button with inline form (type selector, name, content, TTL, proxy toggle). ### Tab 2: Registered Domains - Cloudflare Registrar badge - Search & Register button + Transfer button - Domain table: Domain, Expires, Auto-Renew (ON/OFF badge), Days Left, Customer, Status, Actions - Expiring domains (≤90 days): amber background row, red bold days count - All auto-renew ON shown as green badges - "Renew" button on each domain row - **Register a New Domain** panel at bottom: search input + TLD dropdown (.com, .net, .org, .io, .co) + Search button. Availability result preview showing domain name, available badge (green), price, Add to Cart button, and fee breakdown (transfer, renewal, premium status). ### Domain pricing data Cloudflare sells at cost. Verified pricing: .com = $10.46/yr registration, $10.46/yr renewal, $10.46 transfer. No premium markup for standard TLDs. The API endpoint is `GET /accounts/{id}/registrar/domains/{domain}/check` (returns availability, fees, premium status, TLD support). ### API token permissions required for full functionality | Feature | Required Token Permission | |---|---| | Zone DNS view/edit | Zone → DNS → Edit | | Domain list + expiry | Account → Registrar → Read | | Domain renew | Account → Registrar → Admin | | Domain registration/transfer | Account → Registrar → Admin | ## Router Onboarding (MikroTik) Dedicated router onboarding mockup page. Left side: customer selector, generate button, generated ROS7 script with copy/download. Right side: 3-step explainer, router status card, recently onboarded list. See the `mikrotik-onboarding` skill for the full ROS7 implementation pattern, SSH key deployment, and pitfalls. ### VoIPSimplicity Customer Portal Pages (voipsimplicity.com) Build standalone HTML mockups of the VoIP customer portal. Layout: dark theme (#0f172a background, #1e293b cards, #f59e0b amber accent), 4-tab layout (Inventory, Call Handling, Features, Recent Calls), responsive for phone/tablet/desktop. **Mockup convention:** Single file per portal concept. Real data placeholders (Acme Corp, extensions 101-500). No login/auth UI — assume customer is already logged in. Each customer sees only their own data. ### Customer Portal Dashboard | Section | Content | |---|---| | **Stats bar** | Extensions, Devices, Phone Numbers (DIDs), Calls This Month — color-coded cards (cyan, green, amber, violet) | | **📋 Inventory tab** | Device table (name, extension, model, MAC, online/offline status dots), Extensions list (name, ext, device type, Available/On Call/Offline), Phone numbers table (number, label, route target, SMS support) | | **🔀 Call Handling tab** | Visual call flow (Caller → Auto Attendant → Sales/Support/Directory/Reception → Answer/Voicemail), Time-based routing rules, Call queues (members, strategy, waiting count), Ring groups | | **⚙️ Features tab** | Toggle-grid of enabled/disabled features (Voicemail to Email, Simultaneous Ring, Business Hours, Call Recording, Call Forwarding, Analytics, Auto Attendant, SMS) | | **📞 Recent Calls tab** | Activity feed with inbound/outbound/missed/voicemail items, timestamps, durations, transcription preview | ### Inbound Call Flow Diagram (separate page) A customer-facing explanation page showing how inbound calls get routed. NOT an SVG architecture diagram — use pure HTML/CSS with flexbox flow boxes and arrows (→ and ↓). Three sections: 1. **Visual flow** — horizontal rows with colored bordered boxes (PSTN=rose, Network=cyan, Platform=amber, Decision=violet, Endpoint=emerald). Show three routing forks (Direct Extension / Ring Group / Call Queue) and three resolutions (Answered / Voicemail / Forwarded). 2. **Step-by-step** — 7 numbered steps explaining the call path 3. **Missed call scenarios** — 4-card grid (Voicemail to Email, Forward to Mobile, Simultaneous Ring, Time-Based Routing) 4. **Call Routing Features table** — 11 features with descriptions **Style:** Same dark theme as portal. Boxes 120-160px wide, hover lift effect. Pure HTML/CSS/JS, no SVG dependencies. ### Auto-Provisioning Demo (interactive mock) Two-panel layout: provisioning form on left, API transaction log on right. Bottom sections for CDR export and customer lookup. **Left panel:** Form fields (Customer Name, Extension, Device Name, MAC Address, Phone Model dropdown), "Run Auto-Provision" button, 4-step indicator (Auth → Create Device → Verify → Done). **Right panel:** Terminal-style log showing simulated API calls with timestamps: - `POST /oauth2/token/` → token received - `POST ?object=device&action=create` → device created - `POST ?object=subscriber&action=read` → verified **Bottom sections:** CDR Automation (scheduled nightly export, last export stats), Customer Lookup (search by name/extension/email with simulated results). **Code pattern:** All JS in same file. `async/await` + `sleep()` for step timing. Log output via DOM appendChild. Step indicators via class swapping. ### Serving VoIP mockups All three VoIP pages served from `/var/www/static/` via Caddy on `core.itpropartner.com`: - `/portal` — Customer portal dashboard - `/call-flow` — Inbound call flow diagram - `/voip-demo` — Auto-provisioning interactive demo Caddy config pattern for static files: ``` @portal path /portal handle @portal { root * /var/www/static file_server } ``` ## Router Detail Drill-Down Page Six-tabbed detail page for a specific router: | Tab | Content | |---|---| | **DNS** (default tab, NEW) | Total queries count, unique domains, active clients, blocked count. Top domains widget (7d). Top clients widget. DNS query log table: time, client IP, domain name, record type (A/AAAA badge), resolved IP, client hostname. Filter by client and time period dropdowns. Search bar. | | DHCP | Full client table: hostname, IP, MAC, type (reserved/dynamic), status (active/offline/expired), lease time, last seen. Search bar. "Reserve" button converts dynamic leases to reservations. Inline "Add Reservation" form at bottom. | | Network | DHCP leases + Interfaces combined on one tab | | Bandwidth | Time period selector (hourly, daily, weekly, monthly, yearly). Sparkline chart area. Summary cards showing total up/down per period. | | Firewall | NAT rules, filter rules, port forwards | | Logs | Real-time system log feed | | Config | Backup, restore, firmware update | DHCP table row colors: Reserved = purple badge, Dynamic = blue badge. Status dots: green=active, gray=offline/expired. Summary cards at top: uptime, CPU (with mini-bar), RAM (with mini-bar), firmware version (with update badge if outdated), temperature. DNS tab is the default active tab (was DHCP before). This is the most-used router management function. ## Network Dashboard Page Summary cards: total routers, online count + percentage, alerts count, average heartbeat latency. Router table with columns: Status (color dot + label), Router name, Customer, Model, Uptime, Tunnel latency, **Installed firmware**, **Latest firmware**, Last seen. Firmware color coding: green badge = current, amber badge = update available, gray = pending. Status colors: green=Online, amber=Alert (missed heartbeat), red=Offline (no heartbeat >60m), gray=Pending (script sent, not yet installed). Right rail panels: Recent alerts (color-coded with timestamps), Top bandwidth usage (progress bars), Quick actions (run command, backup, reboot, firmware update, onboard new). ## Apex Vehicle Registration Form (Live JSON Source) As of Jul 7, 2026, the vehicle database form fetches from a live JSON endpoint. The vehicle data is served at **`https://core.itpropartner.com/vehicles.json`** (HTTPS via Caddy, CORS-enabled, 47 makes, 373 models). The HTML snippet in WPForms should load this JSON URL dynamically instead of having data embedded inline. This decouples the form from the data — when the exotic-vehicle-scout cron job discovers new 500+ HP vehicles, I update the JSON file on Core and the form picks it up on next load without any WPForms edits. ### Custom/homebuilt vehicle entry requirement (Jul 10, 2026) The exotic-vehicle-scout cron identified that boutique manufacturers (Ruf, Zenvo, Hennessey) and prototype/one-off builds don't fit the JSON-driven dropdown model. The registration form MUST also support: 1. **A "Not Listed" / "Custom Build" toggle** at the bottom of the make dropdown (or as a separate radio option) 2. When toggled, show **free-text fields** for: Make, Model, Year (or approximate year), and HP (user-entered) 3. Make it clear that the vehicle must still meet the **500+ HP minimum** for Apex Track Experience eligibility — but shouldn't block the submit. Let validation happen server-side or in a confirmation step. The canonical database (`/root/portal-mockup/vehicles.json`) captures production vehicles. Custom/homebuilt cars won't be in it. The registration form must handle this at the UI level, not the DB level. **Real-world examples that need this:** Ruf B8 prototype (1,000+ HP flat-eight mule), Zenvo Aurora Tur (1,850 HP boutique hypercar), Hennessey Venom F5 (1,817 HP) — these exist in the DB already but next year's one-off builds likely won't. See `references/vehicles-database-inventory.md` for the full inventory of all 47 makes, 373 models, and HP ranges. ### Vehicle data URL pattern (Caddy multi-domain) The Caddy config on Core serves all three ITPP subdomains from a single `/etc/caddy/Caddyfile`: ``` sign.itpropartner.com → reverse_proxy to DocuSeal (port 3000) + static files at /var/www/static core.itpropartner.com → static JSON (vehicles.json), health endpoint, capabilities page app.itpropartner.com → reverse_proxy to portal mockups (port 8081) ``` **Tailscale Serve conflict:** Caddy and Tailscale both want port 443. Before starting Caddy for the first time, run `tailscale serve off` to release port 443. `app1.tailc2f3b0.ts.net` and `vaultwarden.tailc2f3b0.ts.net` stop working after this — all HTTPS traffic goes through Caddy. Portal mockups move to `app.itpropartner.com`. ### Writing Caddyfiles Due to my tool constraints, I cannot use `write_file` to create or update `/etc/caddy/Caddyfile` (refused as "sensitive system path"). Use terminal to write the file via heredoc or Python, then run `caddy fmt --overwrite` and `systemctl reload caddy`. After writing, verify with `curl -s http://localhost:2019/config/ | python3 -c "import sys,json; d=json.load(sys.stdin)"` to confirm all routes are loaded. If a new host is added to the Caddyfile but only the old hosts appear when checking the config API, a **full restart** (`systemctl stop caddy && systemctl start caddy`) may be needed instead of `reload`. Reload may only apply changes to existing hosts, not add new ones. Simplified to email + screenshot upload only. Vehicle/class data matched by email from external Google Sheet. N8N automation: screenshot -> GPT-4o vision -> Google Sheet -> auto-updating leaderboard. ## Vehicle database pattern (static JSON) Pre-load make/model/year/HP data as a static JSON file. No external API. Cascading dropdowns with disabled-state chaining. Color-locked specs panel (specs hidden until color selected). Light theme variant available (#ffffff bg, #2563eb accent). ### WPForms integration for vehicle selector When the vehicle selector is embedded inside a WPForms form (not Elementor Forms): 1. **Do NOT rely on hidden `` fields** — WPForms only submits its own managed fields. Hidden inputs outside WPForms' `
` are invisible to the submission handler. 2. **Create a WPForms Single Line Text field** with CSS class `apex-vehicle-field` — this becomes the vehicle data carrier. 3. **The JS snippet** (embedded in a WPForms HTML Content field on the same page) finds this field via `document.querySelector('.apex-vehicle-field input, .apex-vehicle-field textarea')` and populates it with a string like `"2022 Red Ferrari F8 Tributo 710"` when the user selects a vehicle. 4. **Add this JS after the `vrColor` change handler** in the vehicle snippet — it must write to both the visual spec panel AND the WPForms field. 5. **Avoid inline `onchange` handlers** — WordPress/Elementor loaders can strip them. Use `.onchange = function() {...}` or `addEventListener()` inside an IIFE. 6. **Embed the vehicle data inline** as a JS object literal, not fetched via `fetch()` — Elementor HTML widgets may not execute async fetches reliably. 7. **Use the `w()` helper function to write to WPForms** — the `w()` function already runs on every selection change. Add the WPForms population there so it fires for make, model, year, AND color changes, not just the final color select. This prevents stale data. 8. **User setup steps:** User adds the Single Line Text field in WPForms builder → CSS Classes = `apex-vehicle-field` → replaces the old HTML snippet with the updated one → field auto-populates. - `user setup steps:` — The user imports the snippet INTO a WPForms HTML Content field (not into a page builder widget). This is the canonical integration point. - `wpforms notification gating issue` — If the form uses PayPal Commerce or another payment addon, notifications may be set to fire only after payment confirmation. When using "Pay Offline" as a payment option, check the notification JSON for `"paypal_commerce": "1"` or similar flags. Removing that flag from the notification settings (in the database, not the UI) allows notifications to fire immediately. The flag is stored in `wp_posts.post_content` under `settings.notifications.{id}.paypal_commerce`. Use `unset()` on it and update the post. **Common JS failure when populating WPForms:** - Variable scope: `hp` must be defined in the same function scope as the WPForms line. `d[mk][md][yr]` returns the HP value — assign it to a local variable first. - The user sees the visual spec panel update but the WPForms field stays blank → likely scope issue. Debug by checking if `hp` is defined before the `wpf.value = ...` line. - Test with a simple selector first: if the `wpf` variable is null, the CSS class name doesn't match. Verify by running the selector in browser dev tools. ## User interaction style This user prefers a **"vibe coding"** workflow — describe, build, test, iterate. No project manager, no discovery call, no 3-week development cycle. When they say "build X" or "make it do Y", execute. Do not present multiple options or ask which approach they prefer unless the decision has meaningful trade-offs. Default to a reasonable approach and build it. If the user says "this is too verbose" or "this is terrible", strip down immediately. Concise deliverables with tables and bullet points are preferred over prose paragraphs. Fixed decisions (do not re-ask): - Dark themes for all new pages - Mobile-first responsive design (iPhone + iPad tested) - Single-file HTML mockups (no React/build tools) - System font stack, no external font dependencies - Tailwind CDN for fast styling (but NOT @apply directives — see below) ## Preferred mockup style Dark themes (background ~#0a0a0f, cards ~#111122, accent #f97316 orange). Inter font, clean card-based layouts. Generous padding, muted secondary text. Deliver via Tailscale Serve path, serve with `python3 -m http.server`. ## Security policy reference This user has a binding security policy: read-only default, state change requires approval, no assumed consent between actions, operation log after changes. All portal mockups that involve destructive actions (delete customer, deprovision, firewall changes) should include a confirmation flow. ## Navigation link across mockups When adding new pages, add a DNS link (`href="dns.html"`) in the nav bar of every other mockup that has a nav section. The user expects to navigate between all portal pages. ## Responsive design requirements (EXTENSIVE update from Jul 6-7 session) All mockups must work on **phone** (portrait + landscape), **iPad**, and **desktop**. Use Tailwind responsive prefixes throughout. This is not optional — the user has explicitly tested on their iPhone and iPad. ### General rules 1. **Summary cards** always use `grid-cols-2 md:grid-cols-4` (2 on phone, 4 on desktop). Never `grid-cols-4` or `grid-cols-5` without mobile overrides. 5-card grids become `grid-cols-2 sm:grid-cols-3 md:grid-cols-5`. 2. **Navigation** uses `gap-2 sm:gap-5` and `text-xs sm:text-sm`. Main header padding: `px-2 sm:px-4 md:px-6 py-2 sm:py-3`. 3. **Tables with 6+ columns** must use responsive column hiding with Tailwind breakpoint classes: - `hidden sm:block` — Time, Client (hide on phones) - `col-span-2 sm:col-span-1` — Domain (wider on mobile) - `hidden md:block` — Type, Response (hide on phones + landscape phones) - `hidden lg:block` — Client Hostname, less critical data - Table headers must match their data columns — if a column is hidden at a breakpoint, the `th` must have the same class 4. **Bottom panels** (alerts, bandwidth, quick actions) should be `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` not always 3 columns. 5. **Card spacing** tightens on mobile: reduce padding from `p-4`/`p-5` to `p-2 sm:p-3`/`p-3 sm:p-4`. 6. **Detail page summary cards** (5 cards, e.g. uptime/CPU/RAM/FW/temp): `grid-cols-2 sm:grid-cols-3 md:grid-cols-5`. 7. **Text sizes** should scale down: `text-xl md:text-2xl` for bold counts, `text-base md:text-xl` for h1s. 8. **Tab bars** use `overflow-x-auto` + `white-space:nowrap` to avoid wrapping on mobile. 9. **Buttons in form sections** should stack vertically on mobile using `flex-col sm:flex-row`. 10. **Dropdowns next to search inputs** need to be in a nested flex row on mobile, not full-width and stacked. 11. **Grid gaps** tighten on mobile: `gap-1 sm:gap-3 md:gap-4` instead of a single uniform gap. 12. **Header quick-action dropdowns** should use `self-start md:self-auto` to avoid growing full-width. 13. **Subtitle/description text**: `text-xs sm:text-sm` for subtitle lines under h1s. 14. **Page title font**: `text-lg md:text-xl font-bold` for h1. 15. **Page padding**: `px-4 md:px-6 py-6 md:py-8` for main content area. **CRITICAL: When building tables, identify which columns are vs are NOT needed at each breakpoint early, at the design stage.** Assigning responsive classes (`hidden sm:table-cell`) retroactively requires patching every single `` in the table. For tables with many rows, this is a high-effort, error-prone process. Make the column priority decision before writing the HTML, not after. **PROVEN PATTERN for responsive tables (validated on 3 mockups, works reliably):** ```html
Status Router
... ...
``` **TESTING on real devices:** After building, open the page on the user's actual phone or shrink the browser window to phone width. The most common failure is `hidden sm:block` applied to a `` that should be `hidden sm:table-cell` — these are NOT interchangeable. `block` changes the display model and breaks table layout. Always use `hidden sm:table-cell` for table cells. ### HTML tables vs `display: grid` for tabular data **Tables (``) are preferred over `display: grid` for data tables.** Tables with ``/`` handle column-width behavior more predictably across email clients and simpler renderers. Grid-based tables (using `grid grid-cols-N`) work for basic layouts but don't respond well when columns need to hide at breakpoints — you end up having to rewrite every row. Use `
` with `
` column headers and `` cells, and apply responsive hiding via `hidden sm:table-cell` / `hidden md:table-cell` on both `` and corresponding `` elements. **Table wrapper**: Always wrap tables in `
` with `overflow-x:auto; -webkit-overflow-scrolling:touch;` CSS to enable horizontal swipe on narrow screens. This ensures columns that can't fit get scrollable rather than broken. ### Specific page patterns | Page | Tablet columns | Phone columns | |---|---|---| | DNS summary | 4 | 2 | | DNS zones table | 5 cols (responsive column hide) | 3 cols (zone + status + manage) | | Domains table | 5 cols | 2 cols (domain + renew) | | Network summary | 4 | 2 | | Router table | 7 cols (responsive column hide) | 4-5 cols (router + model + status + firmware) | | Router detail cards | 5 → 3 | 2 | | Router detail DNS stats | 4 | 2 | | Router detail top domains/clients | 2 | 1 (stacked) | | Bandwidth periods | 4 → 2 | 2 | ## Debt Recovery Portal Pages (debtrecoveryexperts.com) ### Serving structure | Subdomain | Root | Auth | Content | |-----------|------|------|---------| | `portal.debtrecoveryexperts.com` | `/var/www/capabilities` | Public (Turnstile) | debt-recovery.html (intake), login.html, fee-calculator.html | | `internal.debtrecoveryexperts.com` | `/var/www/internal` | Cloudflare Access SSO | index.html, dre-dashboard.html, dre-case-aging.html, letter-queue.html, dre-client-dashboard.html | ### Cloudflare Access pattern - Internal portal protected by Cloudflare Access (SSO + JWT) - Redirects to `iamgmb.cloudflareaccess.com` for auth - Policy `Team`: allow emails from `germainebrown.com` and `yahoo.com` domains - Caddy config: `internal.debtrecoveryexperts.com { root * /var/www/internal; try_files {path} {path}.html /index.html; file_server }` - **File permission gotcha:** `write_file` creates files as `600` (root-only). Caddy runs as non-root → 403. ALWAYS run `chmod 644` on new HTML files served by Caddy. ### File permission gotcha — Caddy 403s `write_file` creates files with mode `600` (root-only). If Caddy is serving them, it runs as a non-root user and cannot read the file → **403 Forbidden**. **Always run `chmod 644 /path/to/file` after creating any HTML/CSS/JS file that Caddy serves.** When diagnosing a 403 from a Caddy-served page, check file permissions first. If the file is `-rw-------` (600), that's the cause. ### Tailwind `@apply` in inline `