23 KiB
SVS MSP CALC — Beta Build Checkpoint
Date: 2026-03-15 Status: Phases 1–8 + Stage 8 complete. Beta + a11y/perf audit + code quality passes I & II + test expansion + print enhancements done. Tests: 254/254 passing Build Prompt: .claude/plans/STAGE2-BUILD-PROMPT.md Previous Stage Prompt: docs/STAGE3-SESSION-PROMPT.md Previous Stage Prompt: docs/STAGE5-SESSION-PROMPT.md Previous Stage Prompt: docs/STAGE6-SESSION-PROMPT.md Previous Stage Prompt: docs/STAGE7-SESSION-PROMPT.md Previous Stage Prompt: docs/STAGE8-SESSION-PROMPT.md
Completed
Phase 1: Bug Fixes (6/6)
| # | Issue | File | Change |
|---|---|---|---|
| 1.1 | ADDON_INKY default $5 → $8 | quote-pricing.js:12 | ADDON_INKY: 5 → 8 |
| 1.2 | Onboarding fee loses manual override on term switch | SVS-MSP-Calculator.js:41-70 | Store manual value in data-manual-value before 24mo clears it; restore on switch back to m2m/12mo |
| 1.3 | VoIP fax CSV comment misleading | package-prices.csv:18 | "Flat/mo" → "Per seat/mo" |
| 1.4 | Print forces HST on regardless of user toggle | quote-export.js:12 | Removed state.hstEnabled = true; — print now respects user's HST toggle |
| 1.5 | JSON export missing schema version | quote-export.js:229 | Added version: '1.0' as first field in payload |
| 1.6 | ZT admin supplement triggers with no warning | quote-render.js:494-499 | New amber nudge when ztActive warns about $250 admin supplement |
Test expectations updated in test-quote-engine.js for INKY $8 (4 values changed).
Phase 2: Visual Polish (Sections I–III)
| # | Issue | File | Change |
|---|---|---|---|
| 2.1a | Hardcoded #e06070 danger icon |
components.css:41 | → var(--text-danger) — adapts per theme |
| 2.1b | Hardcoded #86efac pill-savings on checked state |
components.css:442 | → var(--text-pill-savings-active) — new token |
| — | Token added to all 4 themes | tokens.css, light.css, glass.css, 70retro.css | Dark: #86efac, Light: #d4f5e0, Glass: #a8f0c8, Retro: #e0f0d0 |
| 2.1c | QUICK-REF.md outdated | docs/QUICK-REF.md | Updated INKY $8, export desc, theme list, test count |
Audit findings (no action needed):
- All 4 themes fully token-covered for Sections I–III
- Glass theme uses
!importantselector overrides (valid for glassmorphism effects) - Sidebar focus-toggle white rgba values sit on colored header — correct everywhere
- No remaining hardcoded colors in Sections I–III component CSS
- Sidebar renders correctly across all 4 themes at all breakpoints
Phase 3: UX Hardening (Sections I–III)
3.1 Interaction Refinements
| # | Change | Files | Details |
|---|---|---|---|
| 3.1a | Smooth theme-switch transition | tokens.css, theme-manager.js | body.theme-transitioning class enables 0.25s color/bg/border fade; applied for 300ms during toggleTheme() |
| 3.1b | Nudge crossfade on rotation/nav | components.css, quote-render.js | .nudge-fading class fades opacity to 0; cycleNudge() does fade-out → swap → fade-in (180ms); auto-rotation now uses cycleNudge(1) for consistency |
| 3.1c | Summary badge fade-in on collapse | components.css | @keyframes badgeFadeIn — 0.25s opacity + translateY animation on .sec-summary-badge |
| 3.1d | Addon toggle micro-feedback | components.css | @keyframes addonPulse — 0.2s scale(1.015) pulse on .addon-row.selected |
3.2 Responsive Edge Cases
| # | Change | Files | Details |
|---|---|---|---|
| 3.2a | Touch targets ≥44px on mobile | responsive.css | .mobile-panel-close-btn 36→44px, .nudge-nav-btn 34→44px at ≤1100px; .collapsible-header and .section-toggle min-height 44px at ≤600px |
| 3.2b | Container query fallback verified | — | @container (max-width: 760px) for addon rows has adequate fallback via ≤600px media query; no change needed |
3.3 Mobile Experience Completeness
| # | Change | Files | Details |
|---|---|---|---|
| 3.3a | Focus trap in mobile panel | mobile-sync.js | trapFocus() function keeps Tab cycling within open panel; focus moves to close button on open, returns to pill on close |
| 3.3b | Safe-area insets for notch phones | responsive.css | padding-bottom: env(safe-area-inset-bottom) on .mobile-panel-sheet; right: max(14px, env(safe-area-inset-right)) on .mobile-quote-pill |
GATE: 88/88 tests pass. All JS syntax-checked. CSS brace balance verified.
Phase 4: Documentation & QA
| # | Task | Status |
|---|---|---|
| 4.1 | Update all docs (README, code-verification, quote-rules, phase-roadmap, QUICK-REF, MASTER-SESSION-PROMPT, ai-session-brief) | COMPLETE |
| 4.2 | Full regression checklist walkthrough | COMPLETE — 88/88 automated, 15/15 manual items verified in code |
| 4.3 | Beta definition of done verification | COMPLETE — all 13 criteria pass |
Docs updated:
- README.md — phase status, 88 tests, 4 themes, export description, file map (70retro.css added)
- code-verification.md — date, test count, all Phase 1-3 changes as known-good baseline
- quote-rules.md — onboarding manual override persistence, HST print behavior, JSON export rules, admin nudge
- phase-roadmap.md — Phases 1-4 status, 88 tests
- QUICK-REF.md — test count in "Remind User", 70retro.css in CSS file map
- MASTER-SESSION-PROMPT.md — 88 tests (3 occurrences), 4 themes (6 occurrences), 70retro.css in tree, 4 theme override layers
- ai-session-brief.md — test count updated
Regression checklist results:
- Automated: 88/88 pass
- Manual: All 15 items verified via source code review (admin waive displays, term/onboarding logic, manual override persistence, sidebar sync, mobile panel sync, persistence round-trip, reset behavior, print HST, JSON export, section headers, theme transitions, nudge crossfade, focus trap, safe-area insets, touch targets)
Beta Definition of Done: ALL 13 CRITERIA PASS
GATE: PASSED — Beta build for Sections I–III is complete.
Phase 5: Performance & Accessibility Audit
| # | Fix | File(s) | Details |
|---|---|---|---|
| A2 | aria-expanded on section & collapsible toggles |
HTML, SVS-MSP-Calculator.js | Added aria-expanded="false" to 12 toggle elements; JS updates dynamically on toggle |
| A3 | Focus trap on reset confirm modal | quote-persistence.js | trapFocusInModal() — Tab cycles within modal when open |
| A4 | aria-label on stepper buttons |
HTML | All 12 step-btn elements have descriptive labels (e.g. "Decrease users") |
| P1 | Glass theme scroll jank on mobile | glass.css | background-attachment: scroll at ≤1100px — avoids fixed-bg repaint on iOS |
| P2 | Skip mobile sync on desktop | mobile-sync.js | Guard skips 35+ element sync when panel closed on desktop; forces full sync on openMobilePanel() |
| M1 | sidebarFocusClientName not in sync map |
mobile-sync.js | Added to html sync list — client name now updates in mobile panel |
| M2 | sl-discount-detail + sl-value-onboarding-label not in sync map |
mobile-sync.js | Added to html sync list — contract term label and onboarding label now sync |
Not flagged (clean): Token coverage, :focus-visible, mobile focus trap, escape handling, touch targets, will-change usage, print CSS isolation, no unused JS.
GATE: 88/88 tests pass. All fixes verified.
Font Awesome Icon Fix
| # | Fix | File | Details |
|---|---|---|---|
| FA1 | Icons invisible on file:// protocol |
components.css:44-79 | All 36 FA Sharp Solid SVG file references converted to inline data:image/svg+xml URIs — eliminates CORS/file:// restriction on mask-image: url() |
Root cause: CSS mask-image: url("fontawesomekit/svgs/...") is blocked by browser security on the file:// protocol. Inline data URIs bypass this completely.
GATE: 88/88 tests pass. Icons render on local file open.
Phase 6: Code Quality Pass (Stage 3)
| # | Fix | File(s) | Details |
|---|---|---|---|
| CQ1 | New --sky color token |
tokens.css, light.css, glass.css, 70retro.css | Per-theme sky/info accent: Dark #38bdf8, Light #0e7490, Glass #7dd3fc, Retro #a34a14 |
| CQ2 | New --transition-fast token |
tokens.css | 0.15s — replaces hardcoded timing in layout.css button transitions |
| CQ3 | Consolidated duplicate button CSS | layout.css:25-47 | .btn-reset-quote and .btn-import-quote shared 10 identical properties → merged into grouped selector |
| CQ4 | Hardcoded amber hover → token-derived | layout.css:43-46 | rgba(232,146,15,…) → color-mix(in srgb, var(--amber) …%, transparent) |
| CQ5 | Hardcoded sky blue hover → token-derived | layout.css:47-50, light.css, glass.css, 70retro.css | All rgba(56,189,248,…) / #38bdf8 / #7dd3fc / #a34a14 → var(--sky) + color-mix() |
| CQ6 | Dead null-check removed | quote-render.js:533 | `nudgeIndex == null |
Audit findings (no action taken — documented for future):
fmt()duplicated in quote-render.js and quote-export.js (both inside IIFEs — intentional isolation, one-liner)- Spacing magic numbers (14px/16px/20px) used 95+ times — too many touchpoints for surgical migration
console.warn()statements in pricing/persistence/import are intentional error reporting- No dead functions, no unreachable code, no unused exports across all 8 JS modules
GATE: 88/88 tests pass. All 4 themes verified tokenized.
Phase 7: Test Coverage Expansion (Stage 4)
| # | Test Group | Count | Details |
|---|---|---|---|
| T1 | Pricing DEFAULTS integrity | 34 | All required keys exist, types correct, values match spec, frozen, ordering invariants |
| T2 | Engine edge cases & boundaries | 55 | Admin fee thresholds, large counts (100u/100ep), string coercion, invalid inputs (NaN/null/empty), servers-only, VoIP-only, VoIP edge cases, ZT without user addon, admin waived, all addons combined, BYOL term independence, discount rounding |
| T3 | Export JSON schema validation | 18 | Payload structure, field types, version field, contract term labels, licensing labels, pricing sub-object, voip tier null handling |
| T4 | Persistence state shape | 6 | JSON round-trip for strings/numbers/booleans, engine compatibility, zero-state |
| T5 | Import payload mapping | 12 | Contract term reverse-map, full export→import→engine round-trip (MRR, effectiveMrr, mrrWithHst, userTotal, endpointTotal, voipTotal, adminFeeNet, effectiveAnnual) |
| T6 | Quote output invariants | 24 | 6 configs × 4 invariants (effectiveMrr, effectiveAnnual, mrrWithHst, non-negative values) |
Total: 88 → 250 tests (162 new). All passing.
GATE: 250/250 tests pass.
Phase 8: Enhanced Print/PDF (Stage 4)
| # | Enhancement | File(s) | Details |
|---|---|---|---|
| P1 | Quote notes field | HTML:920, components.css, quote-persistence.js, quote-export.js, quote-import.js | <textarea id="quoteNotes"> in sidebar, persisted in localStorage, included in JSON export/import, rendered on print invoice |
| P2 | Explicit validity date | quote-export.js | Computes 30-day expiry: "Valid until [date]" in print footer instead of generic "30 days" |
| P3 | Page break control | quote-export.js (inline CSS) | page-break-inside:avoid on table rows + .tots-wrap; break-inside:avoid on notes section |
| P4 | Rep name field | HTML:100, layout.css, quote-persistence.js, quote-export.js, quote-import.js | <input id="repName"> below client name, persisted, in JSON export/import, shown in print header + footer |
| P5 | CYA "Not Included" section | quote-export.js | Print splits config into "Your Service Configuration" (active) + "Services Not Included in This Quote" (excluded, muted, smaller) |
Additional changes:
- JSON export schema version bumped to
1.1(newrepName,quoteNotesfields) - JSON import handles new fields gracefully (backward-compatible with
1.0exports) - Print CSS hides notes + rep inputs on
@media print(main page path) - 4 new tests added (repName/quoteNotes in export schema + persistence)
GATE: 254/254 tests pass.
Key Files to Read on Resume
docs/MASTER-SESSION-PROMPT.md— full architecture and constraintsdocs/QUICK-REF.md— compact file map, IDs, pricingdocs/regression-checklist.md— test procedures.claude/plans/STAGE2-BUILD-PROMPT.md— the build prompt driving this work- This file — checkpoint status
Stage 5 / Phase 9: Visual QA + Retro Theme Overhaul
Visual QA: 3 breakpoints (mobile ~375px, desktop ~1100-1400px, wide ~1800px+) × 4 themes.
| Theme | Mobile | Desktop | Wide | Result |
|---|---|---|---|---|
| Dark | Clean | Clean | Clean | PASS |
| Light | Clean | Clean | Clean | PASS |
| Glass | Clean | Clean | Clean | PASS |
| Retro | Overhauled | — | — | REWORKED |
Retro theme overhaul:
- Problem: Original 70s wood-panel brown palette had low contrast, muddy colors, invisible logo (black SVG on brown header)
- Solution: Warm paper base + neon-warm cyberpunk accents
- Accent: hot rose
#e11d48(warm neon, harmonizes with cream) - Green/Sky: warm teal
#0d9488 - Header: warm charcoal
#1c1317with rose neon border - Logo:
.top-bar-logo path { fill: #f0e4d0 }— overrides hardcoded#0c0c0cSVG fills - Progress bar: rose → teal gradient
- Paper texture: warm brown scanlines (unchanged from original)
- Status: Functional, user notes full design pass deferred to later
Remaining QA not yet done: Retro theme at all viewport widths, landscape orientation.
GATE: 254/254 tests pass. No visual bugs found on Dark/Light/Glass.
Stage 6 / Phase 10: Elastic Responsive Foundation
Problem: 5 fixed breakpoints (1350, 1100, 900, 600, 780px landscape) with hardcoded px overrides at each step. Max width capped at 1800px — wasted space on 1440p+ monitors.
Solution: Fluid clamp() tokens replace discrete breakpoint steps. Only structural breakpoints remain.
| # | Change | File(s) | Details |
|---|---|---|---|
| E1 | Fluid layout tokens | tokens.css | --page-max-width: clamp(1200px, 92vw, 2400px), --page-gutter-x: clamp(16px, 3vw, 80px), --layout-column-gap: clamp(24px, 3vw, 56px), sidebar min 400→360px |
| E2 | Fluid section tokens | tokens.css | --section-offset: clamp(52px, 7vw, 104px), --section-num-width/size fluid, --section-padding-* fluid |
| E3 | Eliminated 1350px breakpoint | responsive.css | Removed — fluid tokens handle narrow desktop scaling |
| E4 | Eliminated 900px breakpoint | responsive.css | Removed — fluid tokens handle tablet spacing/numerals |
| E5 | Fluid logo margin | base.css | margin-left: clamp(26px, 5.2vw, 78px) replaces hardcoded 78px + breakpoint overrides |
| E6 | Fluid main-col gap | layout.css | gap: clamp(16px, 1.5vw, 24px) replaces hardcoded 24px + breakpoint override |
| E7 | Fluid client-bar padding | layout.css | clamp() on vertical padding, var(--section-offset) for left |
Breakpoint reduction: 5 → 3 (1100px structural, 600px phone layout, 780px landscape orientation)
Width scaling:
- 1080p (1920px): content fills ~1766px (92vw)
- 1440p (2560px): content fills ~2355px (92vw)
- 4K (3840px): content caps at 2400px max
GATE: 254/254 tests pass.
Stage 7 / Phase 11: Feature Work (Option A)
11.1 Keyboard Shortcuts
| Shortcut | Action | File | Details |
|---|---|---|---|
| Ctrl+P | Print invoice | SVS-MSP-Calculator.js | preventDefault() blocks browser print dialog; calls printInvoice() |
| Ctrl+E | Export JSON | SVS-MSP-Calculator.js | Calls exportQuoteJSON() |
| Ctrl+R | Reset quote | SVS-MSP-Calculator.js | Opens confirm modal via openResetConfirm() — not a hard reset |
| Escape | Close overlays | mobile-sync.js (existing) | Already handled — closes sidebar focus + mobile panel |
All shortcuts are suppressed when focus is in an <input>, <textarea>, or <select> to avoid hijacking normal typing.
11.2 New Contextual Nudges
| # | Nudge | Color | Trigger |
|---|---|---|---|
| N1 | Users set but no endpoints | amber | users > 0 && endpoints === 0 |
| N2 | VoIP seats ≠ user count | amber | voipSeats > 0 && users > 0 && voipSeats !== users |
| N3 | High admin-to-MRR ratio | amber | adminFeeNet > MRR * 0.25 (and not waived) |
| N4 | Extended Hours upsell | green | !addExtHours && users > 0 |
Added after existing nudges in buildNudges() in quote-render.js (lines 524–551).
GATE: 254/254 tests pass.
Stage 8 / Phase 12: Code Quality Pass II
8.1 --transition-fast / --transition-medium Token Adoption
| # | Change | File(s) | Details |
|---|---|---|---|
| T1 | New --transition-medium token |
tokens.css | 0.25s — for chevron/collapsible/nudge transforms |
| T2 | 10× 0.15s → var(--transition-fast) |
components.css | pill-toggle, tier-seg, addon-preview-pill, addon checkbox, sidebar-focus-toggle, nudge-nav-btn, btn-toggle-all, quote-notes-input, btn-export |
| T3 | 3× 0.25s → var(--transition-medium) |
components.css | sec-chevron transform, collapsible-toggle transform, nudge-banner bg/border |
| T4 | 1× 0.15s → var(--transition-fast) |
base.css | theme-toggle-btn |
| T5 | 2× 0.15s → var(--transition-fast) |
responsive.css | mobile-quote-pill, mobile-panel-close-btn |
Left as-is: 0.12s (stepper/addon micro-interactions), 0.18s (term tile tuned), 0.2s (switch/section/overlay), 0.3s (progress bar/accordion), 0.34s (section-body tuned bezier). No 0.15s hardcodes remain outside the token definition.
8.2 CSS Selector Specificity Audit
| # | Change | File(s) | Details |
|---|---|---|---|
| S1 | .sec-open → .section.sec-open |
components.css | Removed 2× !important — specificity now beats .section:hover via class count |
| S2 | Documented intentional !important |
components.css | Added comments to .qs-discount-sub, sidebar utility classes (.sl-muted, .sl-discount-val, .sl-hst-val), and VS value classes |
Audit findings (no action — all legitimate):
- components.css: 13 remaining
!important— all utilitydisplay: noneor color overrides that must beat compound parent selectors - 70retro.css: 37
!important— theme override pattern (same as glass.css with 97) - responsive.css: 8
!important— mobile sidebar embedding - tokens.css: 1
!important—body.theme-transitioning(intentional, per spec) - print.css: All
!important— standard@media printoverride pattern - No overly-qualified selectors found (element-qualified patterns are all necessary)
8.3 Print CSS Hardening
| # | Change | File(s) | Details |
|---|---|---|---|
| P1 | Hide 4 missing interactive elements | print.css | Added display: none !important for .sidebar-focus-toggle, .sidebar-utility, .qs-switch, .confirm-modal |
| P2 | Theme-independent callout borders | tokens.css, print.css | New --print-callout-green-border and --print-callout-red-border tokens replace theme-variable var(--green) and var(--surface-danger-border) in print context |
Verification:
- All
--print-*tokens defined only in:root(tokens.css) — no theme overrides - Page-break rules unaffected by fluid layout tokens (
.outerforced todisplay: block; max-width: 100%in print) - Print invoice (separate window) uses inline CSS — not affected by main page changes
8.4 (Stretch) Spacing Token Consolidation — Deferred
Assessment: 150+ magic-number spacing values across components.css (10px: 36, 12px: 35, 14px: 36, 16px: 24, 20px: 19). Existing --space-stack-* tokens used only 4× out of 150+. Migration scope too broad for surgical approach. Deferred to a dedicated spacing-focused stage.
GATE: 254/254 tests pass.
Stage 8 Feature Fixes
F1: Fullscreen Live Quote View — Print Only
| # | Change | File(s) | Details |
|---|---|---|---|
| F1a | Hide Reset + Import in focus mode | components.css | .export-wrap and .sidebar-utility now display: none in sidebar-focus-open |
| F1b | Print button inside sidebar header | HTML, components.css | New .sidebar-focus-print-btn in .sidebar-header-row — hidden by default, display: inline-flex in focus mode |
| F1c | Print button hidden in print/mobile | print.css, components.css | display: none !important in @media print and .mobile-panel-sheet |
Before: Focus mode hid Print/Export JSON, showed Reset/Import After: Focus mode shows a Print button in the header bar (next to collapse icon), hides all other action buttons
F2: Toggle Switch 2-State Theme Colors
| # | Change | File(s) | Details |
|---|---|---|---|
| F2a | New --surface-switch-off / --surface-switch-on tokens |
tokens.css | Dark: off #4a4540, on var(--green) |
| F2b | Light theme switch tokens | light.css | Off #b5ad9f, on var(--green) |
| F2c | Glass theme switch tokens | glass.css | Off rgba(255,255,255,0.15), on var(--green) |
| F2d | Retro theme switch tokens | 70retro.css | Off #c0b4a0, on var(--green) |
| F2e | Component CSS uses tokens | components.css | .qs-switch bg → var(--surface-switch-off), checked → var(--surface-switch-on) |
| F2f | Glass checked override | glass.css | Added .qs-toggle-row input:checked ~ .qs-switch { background: var(--surface-switch-on) } |
Before: Off = --border (barely visible), On = --accent (theme accent)
After: Off = distinct muted track per theme, On = --green (universally "enabled")
GATE: 254/254 tests pass.
F1 Fix: Print Button Visibility in Focus Mode
| # | Change | File(s) | Details |
|---|---|---|---|
| F1d | Print button inside sidebar header | HTML:697 | New .sidebar-focus-print-btn button in .sidebar-header-row, between title and collapse icon |
| F1e | Focus-only visibility | components.css | display: none by default; display: inline-flex when body.sidebar-focus-open |
| F1f | Hidden in print + mobile | print.css, components.css | display: none !important in @media print and .mobile-panel-sheet |
Root cause: .sidebar-utility is a sibling of .sidebar, not inside it. When .sidebar becomes position: fixed, the utility div is left behind the backdrop.
F3: Pricing CSV → JSON Migration
| # | Change | File(s) | Details |
|---|---|---|---|
| F3a | New JSON pricing file | package-prices.json | Structured by category with { key: { value, description } } format — human-readable + machine-parseable |
| F3b | Script-loaded pricing | package-prices-data.js, HTML | window.SVS_PRICING_DATA set via <script> tag — works on file:// protocol, no web server needed |
| F3c | Loader updated | quote-pricing.js | loadPricing() checks SVS_PRICING_DATA global first (script path), then fetch() fallback (web server), then built-in defaults |
| F3d | CSV retained | package-prices.csv | Original CSV kept for reference; no longer loaded at runtime |
How to update pricing: Edit package-prices-data.js — change the value field for any key. No web server needed. The file is loaded via <script> tag before the pricing engine initializes.
JSON format example:
{
"user_packages": {
"RATE_M365": { "value": 130, "description": "Per-user/mo rate — M365 included" }
}
}
GATE: 254/254 tests pass.
Hard Constraints (reminder)
- DOM IDs are a contract — no renaming
- 254 tests must pass:
node svsmspcalc/tests/test-quote-engine.js - localStorage keys unchanged
- All 4 themes must work after every change
- Mobile parity maintained
- No frameworks, no npm — vanilla only
- Surgical changes only
- Sections IV–VI unchanged (deferred)