App Pages Overhaul + Growth Intelligence Analytics — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Redesign the account page, add growth intelligence analytics, and bring all pages to a consistent design standard using shared components.

Architecture: Design-system-first approach. Define new shared CSS components (stat-card-v2, section-header-v2, status-badge, heatmap), then apply them to the account page (full redesign), analytics page (growth intelligence sections), and a consistency pass across dashboard/templates/domains/team.

Tech Stack: Static Jekyll frontend in docs/, vanilla JS, CSS custom properties, SVG sparklines, CSS Grid heatmaps. Backend: Node.js Lambda in terraform/lambda_src/.


Phase 1: Design System Components

Task 1: Add shared CSS component classes

Files:

Step 1: Add stat-card-v2 CSS

Add after the last rule in style.css:

/* ── Stat Card v2 ────────────────────────────────────────────────────────── */

.stat-card-v2-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.stat-card-v2 {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  backdrop-filter: blur(var(--glass-blur));
  transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card-v2:hover {
  transform: translateY(-2px);
  box-shadow: var(--glow-sm);
}
.stat-card-v2-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.25rem;
}
.stat-card-v2-value {
  font-size: 1.75rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1.2;
}
.stat-card-v2-delta {
  font-size: 0.75rem;
  margin-top: 0.25rem;
}
.stat-card-v2-delta.positive { color: var(--success); }
.stat-card-v2-delta.negative { color: var(--danger); }
.stat-card-v2-delta.neutral { color: var(--text-muted); }
.stat-card-v2-sparkline {
  margin-top: 0.5rem;
  height: 24px;
}
.stat-card-v2-sparkline svg {
  width: 100%;
  height: 24px;
}
.stat-card-v2-sparkline polyline {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.stat-card-v2-sparkline .sparkline-area {
  fill: var(--accent);
  opacity: 0.1;
}

@media (max-width: 640px) {
  .stat-card-v2-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-card-v2-value { font-size: 1.35rem; }
}

Step 2: Add section-header-v2 CSS

/* ── Section Header v2 ──────────────────────────────────────────────────── */

.section-header-v2 {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--border);
}
.section-header-v2-text h3 {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--text);
  margin: 0;
}
.section-header-v2-text p {
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-top: 0.25rem;
}

Step 3: Add status-badge-v2 CSS

These expand on the existing .status-badge (line 2988) but add the dot indicator pattern:

/* ── Status Badge v2 ────────────────────────────────────────────────────── */

.status-dot {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.8rem;
  font-weight: 500;
}
.status-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.status-dot-active::before { background: var(--success); }
.status-dot-trialing::before { background: var(--accent); }
.status-dot-past-due::before { background: #f59e0b; }
.status-dot-canceled::before { background: var(--danger); }
.status-dot-verified::before { background: var(--success); }
.status-dot-pending::before { background: #f59e0b; }
.status-dot-inactive::before { background: var(--text-muted); }

Step 4: Add heatmap CSS

/* ── Signup Heatmap ──────────────────────────────────────────────────────── */

.analytics-heatmap-grid {
  display: grid;
  grid-template-columns: auto repeat(24, 1fr);
  gap: 2px;
  margin-top: 1rem;
}
.analytics-heatmap-label {
  font-size: 0.65rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  padding-right: 0.5rem;
  white-space: nowrap;
}
.analytics-heatmap-cell {
  aspect-ratio: 1;
  border-radius: 2px;
  min-width: 0;
  position: relative;
}
.analytics-heatmap-cell[data-tooltip]:hover::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.25rem 0.5rem;
  font-size: 0.65rem;
  white-space: nowrap;
  pointer-events: none;
  z-index: 10;
}
.analytics-heatmap-legend {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
  font-size: 0.65rem;
  color: var(--text-muted);
}
.analytics-heatmap-legend-bar {
  display: flex;
  gap: 2px;
}
.analytics-heatmap-legend-bar span {
  width: 12px;
  height: 12px;
  border-radius: 2px;
}

@media (max-width: 640px) {
  .analytics-heatmap-grid { overflow-x: auto; min-width: 500px; }
}

Step 5: Add velocity panel and insights CSS

/* ── Velocity Panel ──────────────────────────────────────────────────────── */

.analytics-velocity-stats {
  display: flex;
  gap: 2rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}
.analytics-velocity-stat {
  display: flex;
  flex-direction: column;
}
.analytics-velocity-stat-value {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--text);
}
.analytics-velocity-stat-label {
  font-size: 0.7rem;
  color: var(--text-muted);
  margin-top: 0.125rem;
}

/* ── Insights Card ───────────────────────────────────────────────────────── */

.analytics-insight-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1rem;
}
.analytics-insight-item {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: rgba(99, 102, 241, 0.04);
  border: 1px solid rgba(99, 102, 241, 0.08);
}
.analytics-insight-item.positive { background: rgba(34, 197, 94, 0.04); border-color: rgba(34, 197, 94, 0.08); }
.analytics-insight-item.negative { background: rgba(239, 68, 68, 0.04); border-color: rgba(239, 68, 68, 0.08); }
.analytics-insight-icon { font-size: 1rem; flex-shrink: 0; line-height: 1.4; }
.analytics-insight-text { font-size: 0.85rem; color: var(--text); line-height: 1.4; }
.analytics-insight-text a { color: var(--accent); }

/* ── Account Page v2 ─────────────────────────────────────────────────────── */

.account-section {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  backdrop-filter: blur(var(--glass-blur));
  margin-bottom: 1rem;
}
.account-section-danger {
  border-color: rgba(239, 68, 68, 0.2);
  background: rgba(239, 68, 68, 0.03);
}
.account-field-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.5rem 0;
}
.account-field-row + .account-field-row {
  border-top: 1px solid var(--border);
}
.account-field-label {
  font-size: 0.8rem;
  color: var(--text-muted);
  min-width: 120px;
}
.account-field-value {
  font-size: 0.9rem;
  color: var(--text);
  font-weight: 500;
}
.account-field-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.account-plan-features {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.account-plan-feature {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.8rem;
  color: var(--text-muted);
  padding: 0.25rem 0.5rem;
  background: rgba(99, 102, 241, 0.06);
  border-radius: 9999px;
}
.account-plan-feature::before {
  content: "\2713";
  color: var(--success);
  font-weight: 700;
  font-size: 0.7rem;
}
.account-invoice-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  margin-top: 0.75rem;
}
.account-invoice-table th {
  text-align: left;
  padding: 0.5rem 0.75rem;
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border-bottom: 1px solid var(--border);
}
.account-invoice-table td {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--border);
}
.account-invoice-table tr:last-child td { border-bottom: none; }
.account-card-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
}
.account-masked-key {
  font-family: monospace;
  font-size: 0.85rem;
  background: var(--bg-input);
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

@media (max-width: 640px) {
  .account-field-row { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
  .account-section { padding: 1rem; }
}

Step 6: Commit

git add docs/assets/css/style.css
git commit -m "Design system: stat-card-v2, section-header-v2, status-dot, heatmap, velocity, account-v2 CSS"

Phase 2: Shared JS Helpers

Task 2: Add stat-card-v2 and SVG sparkline builder to app.js

Files:

Step 1: Add helpers

Add before the showEl function (line 450):

// ── Stat Card v2 Builder ────────────────────────────────────────────────

function buildStatCardV2(label, value, delta, sparkData) {
  var card = document.createElement("div");
  card.className = "stat-card-v2";

  var labelEl = document.createElement("div");
  labelEl.className = "stat-card-v2-label";
  labelEl.textContent = label;
  card.appendChild(labelEl);

  var valueEl = document.createElement("div");
  valueEl.className = "stat-card-v2-value";
  valueEl.textContent = value;
  card.appendChild(valueEl);

  if (delta) {
    var deltaEl = document.createElement("div");
    var cls = delta.value > 0 ? "positive" : delta.value < 0 ? "negative" : "neutral";
    deltaEl.className = "stat-card-v2-delta " + cls;
    var arrow = delta.value > 0 ? "\u25B2 " : delta.value < 0 ? "\u25BC " : "";
    deltaEl.textContent = arrow + delta.label;
    card.appendChild(deltaEl);
  }

  if (sparkData && sparkData.length > 1) {
    var sparkContainer = document.createElement("div");
    sparkContainer.className = "stat-card-v2-sparkline";
    sparkContainer.appendChild(buildSvgSparkline(sparkData));
    card.appendChild(sparkContainer);
  }

  return card;
}

function buildSvgSparkline(data) {
  var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  svg.setAttribute("viewBox", "0 0 100 24");
  svg.setAttribute("preserveAspectRatio", "none");

  var max = Math.max.apply(null, data.concat([1]));
  var points = data.map(function (v, i) {
    var x = data.length > 1 ? (i / (data.length - 1)) * 100 : 50;
    var y = 24 - (max > 0 ? (v / max) * 22 : 0) - 1;
    return x.toFixed(1) + "," + y.toFixed(1);
  }).join(" ");

  // Area fill
  var area = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
  var areaPoints = "0,24 " + points + " 100,24";
  area.setAttribute("points", areaPoints);
  area.setAttribute("class", "sparkline-area");
  svg.appendChild(area);

  // Line
  var line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
  line.setAttribute("points", points);
  svg.appendChild(line);

  return svg;
}

function buildStatusDot(status, label) {
  var span = document.createElement("span");
  span.className = "status-dot status-dot-" + status;
  span.textContent = label;
  return span;
}

function buildSectionHeader(title, description, actionBtn) {
  var header = document.createElement("div");
  header.className = "section-header-v2";

  var textDiv = document.createElement("div");
  textDiv.className = "section-header-v2-text";
  var h3 = document.createElement("h3");
  h3.textContent = title;
  textDiv.appendChild(h3);
  if (description) {
    var p = document.createElement("p");
    p.textContent = description;
    textDiv.appendChild(p);
  }
  header.appendChild(textDiv);

  if (actionBtn) header.appendChild(actionBtn);
  return header;
}

Step 2: Commit

git add docs/assets/js/app.js
git commit -m "Shared helpers: buildStatCardV2, buildSvgSparkline, buildStatusDot, buildSectionHeader"

Phase 3: Account Page Redesign

Task 3: Rewrite account.html structure

Files:

Step 1: Replace the account grid HTML

Replace the current account-grid div and its contents with the new stacked-sections layout. Keep the same element IDs for JS binding but restructure into vertical account-section blocks:

Each section uses:

Step 2: Commit HTML

git add docs/account.html
git commit -m "Account page: restructure to stacked sections layout"

Task 4: Rewrite account.js to render new layout

Files:

Step 1: Update renderAccount() to use new element IDs and patterns

Key changes:

Step 2: Update renderPaymentMethod() to use status dots and new layout

Step 3: Update renderBillingHistory() to use invoice table with status dots

Step 4: Update renderPlanFeatures() to use feature pills

Step 5: Commit JS

git add docs/assets/js/account.js
git commit -m "Account page: rewrite JS for stacked sections + status dots + feature pills"

Phase 4: Analytics Growth Intelligence — Backend

Task 5: Expand breakdown endpoint with day×hour heatmap data

Files:

Step 1: Add day-of-week tracking to the pagination loop

In the existing handleGetBreakdown function, add these accumulators before the do loop:

const signupByDayOfWeek = new Array(7).fill(0);
const signupByDayHour = Array.from({ length: 7 }, () => new Array(24).fill(0));

Inside the for (const item of result.items) loop, after the existing signupByHour[hour]++, add:

if (item.created_at) {
  const date = new Date(item.created_at);
  const dayOfWeek = date.getUTCDay(); // 0=Sun, 1=Mon, ..., 6=Sat
  signupByDayOfWeek[dayOfWeek]++;
  signupByDayHour[dayOfWeek][date.getUTCHours()]++;
}

Note: The created_at parsing and hour variable already exist, so just add the day-of-week lines after them.

Step 2: Add velocity stats computation after the pagination loop

After the } while (lastEvaluatedKey); line, add:

// Compute velocity stats
const totalSignups = Object.values(deviceBreakdown).reduce((a, b) => a + b, 0);
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
let bestDayIdx = 0;
let worstDayIdx = 0;
for (let i = 1; i < 7; i++) {
  if (signupByDayOfWeek[i] > signupByDayOfWeek[bestDayIdx]) bestDayIdx = i;
  if (signupByDayOfWeek[i] < signupByDayOfWeek[worstDayIdx]) worstDayIdx = i;
}
let peakHour = 0;
for (let i = 1; i < 24; i++) {
  if (signupByHour[i] > signupByHour[peakHour]) peakHour = i;
}

Step 3: Add new fields to the response

In the return jsonResponse(200, { ... }), add:

signup_by_day_of_week: signupByDayOfWeek,
signup_by_day_hour: signupByDayHour,
velocity: {
  total_signups: totalSignups,
  best_day: dayNames[bestDayIdx],
  best_day_count: signupByDayOfWeek[bestDayIdx],
  worst_day: dayNames[worstDayIdx],
  worst_day_count: signupByDayOfWeek[worstDayIdx],
  peak_hour: peakHour,
  peak_hour_count: signupByHour[peakHour],
},

Step 4: Validate Terraform

Run: cd terraform && terraform validate Expected: Success

Step 5: Commit

git add terraform/lambda_src/analytics.mjs
git commit -m "Breakdown endpoint: add day-of-week heatmap + velocity stats"

Phase 5: Analytics Growth Intelligence — Frontend

Task 6: Add HTML sections for new analytics panels

Files:

Step 1: Add sections between the KPI grid and the main chart

After the analytics-kpi-grid div (line 73) and before the analytics-main-chart div (line 76), insert:

<!-- Signup Velocity -->
<div class="glass-card analytics-panel-card" id="analytics-velocity">
  <p class="loading">Loading velocity...</p>
</div>

After the Growth Insights section (around line 110, before the comparison table), insert:

<!-- Signup Activity Heatmap -->
<div class="glass-card analytics-panel-card" id="analytics-heatmap">
  <p class="loading">Loading heatmap...</p>
</div>

<!-- Geographic + Leaderboard -->
<div class="analytics-two-grid">
  <div class="glass-card analytics-panel-card" id="analytics-timezones">
    <p class="loading">Loading...</p>
  </div>
  <div class="glass-card analytics-panel-card" id="analytics-leaderboard">
    <p class="loading">Loading...</p>
  </div>
</div>

<!-- Actionable Insights -->
<div class="glass-card analytics-panel-card" id="analytics-insights">
  <p class="loading">Loading insights...</p>
</div>

Step 2: Commit

git add docs/analytics.html
git commit -m "Analytics HTML: add velocity, heatmap, leaderboard, insights placeholders"

Task 7: Add aggregateBreakdown expansion + 4 new render functions

Files:

Step 1: Expand aggregateBreakdown() to include new fields

In the existing aggregateBreakdown() function, add accumulators for:

Merge logic: sum the arrays across selected waitlists.

Step 2: Update renderDashboard() to call new functions

After the existing renderGrowthInsights(breakdown) call, add:

renderVelocity(breakdown, viewsByDay, signupsByDay, currentDays, previousDays);
renderHeatmap(breakdown);
renderTimezones(breakdown);
renderLeaderboard(currentDays);
renderInsights(breakdown, viewsByDay, signupsByDay, currentDays, previousDays);

Step 3: Add renderVelocity()

Renders into #analytics-velocity:

Step 4: Add renderHeatmap()

Renders into #analytics-heatmap:

Step 5: Add renderTimezones()

Renders into #analytics-timezones:

Step 6: Add renderLeaderboard()

Renders into #analytics-leaderboard:

Step 7: Add renderInsights()

Renders into #analytics-insights:

Step 8: Commit

git add docs/assets/js/analytics.js
git commit -m "Analytics JS: velocity, heatmap, leaderboard, and rule-based insights"

Phase 6: Dashboard Consistency

Task 8: Replace dashboard summary cards with stat-card-v2

Files:

Step 1: Replace HTML summary grid

Replace the 4 .summary-card divs with a single container:

<div id="dashboard-summary" class="stat-card-v2-grid">
  <div class="stat-card-v2"><div class="analytics-skeleton analytics-skeleton-value"></div><div class="analytics-skeleton analytics-skeleton-label"></div></div>
  <div class="stat-card-v2"><div class="analytics-skeleton analytics-skeleton-value"></div><div class="analytics-skeleton analytics-skeleton-label"></div></div>
  <div class="stat-card-v2"><div class="analytics-skeleton analytics-skeleton-value"></div><div class="analytics-skeleton analytics-skeleton-label"></div></div>
  <div class="stat-card-v2"><div class="analytics-skeleton analytics-skeleton-value"></div><div class="analytics-skeleton analytics-skeleton-label"></div></div>
</div>

Step 2: Rewrite showDashboardSummary()

Use buildStatCardV2() to create 4 cards:

  1. Total Signups — sum of all waitlist signup_counts, 7-day sparkline from stats, delta vs previous period
  2. Today’s Signups — from stats if available (or 0), delta vs yesterday
  3. Viral Rate — referral_signups / total_signups × 100, delta as pp change
  4. Active Waitlists — count of waitlists with signups, plan badge as subtitle text

For sparkline data: If per-waitlist stats endpoint provides daily data, use it. Otherwise, show sparkline only when analytics data is loaded (Pro users).

Step 3: Add mini sparklines to waitlist cards

In the existing waitlist card rendering function, add a small SVG sparkline showing the 7-day signup trend if data is available. This is a bonus enhancement — uses the same buildSvgSparkline() helper.

Step 4: Commit

git add docs/dashboard.html docs/assets/js/dashboard.js
git commit -m "Dashboard: stat-card-v2 summary cards with sparklines + deltas"

Phase 7: Templates, Domains, Team Polish

Task 9: Templates — action empty state + type icons

Files:

Step 1: Replace empty state with action empty state

Replace the current innerHTML empty state with DOM-built action empty state that includes:

Step 2: Add type icons to template cards

In the template card rendering, prepend a small SVG icon based on template type:

Step 3: Commit

git add docs/assets/js/templates.js
git commit -m "Templates: action empty state + type icons on cards"

Task 10: Domains — status dots + copy buttons

Files:

Step 1: Replace status text with status-dot badges

In domain card rendering, replace the plain status-badge span with a buildStatusDot() call:

Step 2: Add copy buttons to DNS record values

In the DNS record display, add a small “Copy” button next to each record value that copies the value to clipboard using navigator.clipboard.writeText().

Step 3: Add progress indicator

Above the domains grid, add a “2 of 3 domains verified” progress text.

Step 4: Commit

git add docs/assets/js/domains.js
git commit -m "Domains: status-dot badges, copy buttons on DNS records, progress indicator"

Task 11: Team — role badges + (You) label + pending status

Files:

Step 1: Add role color coding

Update role badge classes:

Step 2: Add (You) label

Compare member email to current user email (from getUser()). If match, append “(You)” span after the email.

Step 3: Add pending invite badge

If member has status === "pending", show a yellow buildStatusDot("pending", "Pending Invite") badge.

Step 4: Commit

git add docs/assets/js/team.js
git commit -m "Team: role color badges, (You) label, pending invite status"

Phase 8: Verification

Task 12: Final verification pass

Step 1: Terraform validate

Run: cd terraform && terraform validate Expected: Success

Step 2: Jekyll build

Run: cd docs && bundle exec jekyll build Expected: Success (ignore pre-existing Liquid warnings in integration-react.md)

Step 3: Grep for remaining inline display:none

Run grep on all modified files to verify no regressions:

Step 4: Visual consistency check

Verify all pages use consistent patterns:

Step 5: Final commit + push

git push origin main