Skip to content
Metric VaultHelp Center
Open app

Frontend architecture

How dashboard.html is put together - the three-column shell, the 106-view router, the renderer dispatch chain, and every file in js/ and css/ with what it owns.

Last updated 2026-08-06

Summary#

The customer application is a single HTML document. dashboard.html is 7,223,279 bytes and 64,435 lines, uses CRLF line endings, and contains 106 view containers, every one of them present in the DOM from first paint and shown or hidden by a client-side router. There is no framework, no bundler and no build step: the scripts in js/ are classic scripts, the stylesheets in css/ are plain CSS, and load order is source order.

That single-document design is the reason several things look unusual. Global window.* assignment is the module system. A <link> placed 31,000 lines into the body is how one stylesheet is made to beat another. And the twenty-four files in js/ were each lifted out of an inline <script> and re-linked at the exact position they occupied, so that parse and execution order survived the extraction.

Purpose#

The frontend serves one goal: a customer should be able to move between seventy tools without a page load, without losing state, and without the app feeling like seventy products. Everything below follows from that, plus the hard constraint that there is nothing to compile.

Keeping all 106 views in one document means switching tools costs a hidden attribute rather than a network round trip. The price is a very large file and a load order that must be respected. Extracting behavior into js/ reduces that file and makes the pieces independently reviewable, which is why the extraction rules are strict about not changing when anything runs.

The visual half of "one product, not seventy" is a separate topic with its own enforcement; see The design system.

Architecture#

text
  dashboard.html
    ├── #authOverlay              client-side gate, hidden once Supabase resolves
    └── .app  (CSS grid: 92px | 248px | 1fr)
         ├── aside.rail           12 section icons + hover flyout
         ├── aside.side           tool list for the active section, account block
         └── div.main             the only scroll container
              ├── header.topbar   back, favorites, notifications, search,
              │                   credits, country, range, theme, user menu
              ├── nav.mv-crumbs   Dashboard > Section > Tool
              └── 106 × div.view  exactly one visible at a time
                   ├── the tool's own inputs  ([data-in], button[data-run])
                   ├── div.mv-results[data-res]   the result container
                   └── div.mvte                   the tool explainer

The router#

A single IIFE owns navigation. It is plain history API work, no library.

ConcernImplementation
View mapvar views = { key: document.getElementById("view-<key>") }, 106 entries
Default viewdashboard
URL format#/<view> for every view except the default, which gets a clean path so the URL never reads /dashboard#/dashboard
Section mapsVIEW_SECTION and SECTION_FIRST, both derived from SECTIONS at boot
Public APIwindow.mvGoTo(view, push), and window.mvNavTo(view) which routes through it
Legacy alias#imagetool rewrites to imgresize, because the old combined Image Tool was split into Resize and Reformat

goTo(v, push) lights up the rail, re-renders the section panel, marks the nav item active, sets the panel heading, renders breadcrumbs, hides every other .view, resets .main scroll to the top, then pushes or replaces history. Views with no section, reached only from in-app buttons, leave the sidebar alone but still record a route so a reload lands back on them.

Per-view lazy initialisation#

Views that need setup do it once, the first time they become visible. A MutationObserver per .view watches for the hidden attribute being removed and calls window.mvOnViewOpen(v), which runs that screen's init behind a mvIOnce guard. Screens wired this way include kmanager, cmanager, cmonitor, rank, team, scheduled, integrations, auto, myreports, history, account and dashboard.

The tool contract#

Every tool view shares three elements, and the delegated handlers depend on exactly these:

ElementRole
input[data-in]The query field. Enter submits
button[data-run="<tool_type>"] with data-kind="ai" or "tool"The run trigger
div.mv-results[data-res]Where the result is written

runToolButton(btn) is the generic runner. It refuses with Enter a value first. when the input is empty and Please sign in to run this tool. when there is no session, writes Analyzing… this can take a few seconds for live data. into the result container, scrolls it into view, then calls window.callTool or window.callPremiumAI depending on data-kind. On success it stashes the payload on view.__mvLastData, records a recent target, and hands off to window.mvRenderOld. A quota rejection renders Monthly limit reached — upgrade to continue. and anything else renders the escaped error or Something went wrong.

Renderer dispatch#

window.mvRenderOld(res, data, type, query) is the one entry point from a tool run to a rendered result. It adds class="mvx" to the result container and stamps data-theme on it, then walks a fixed chain and stops at the first match:

  1. BES, a map of about forty tool types to a named premium renderer, called as W[name](res, data, opts) where opts is one of {idPrefix, query}, {idPrefix}, {query}, {brand}, or omitted.
  2. AIINTEL (prompt_research, ai_competitor_research, brand_performance, ai_questions, prompt_tracking, media_monitoring) to mvRenderAiIntelPremium(type, res, data).
  3. BATCH6 (ad_clarity, pla_research) to mvRenderBatch6Extras.
  4. INLINE, twelve tools mapped to mv_inline_* functions.
  5. mvRenderAny(res, data, {}), the generic JSON renderer.
  6. A last-resort card reading No legacy renderer for <type>.

After every successful path a shared verified() step runs mvAttachVerifiedHeader and then mvGuideAttach, so the verified-source header and the Get Recommendations panel land on every tool without each renderer remembering to add them. How to write one of these correctly is Building a result renderer.

Result states#

Four states share one vocabulary across all tools: .mv-loading (with a spinner), .mv-error (red tinted), .mv-note (used for quota) and .mvr-empty. Several other components treat a container holding only one of those as "no result yet", which is how the export gate and the exec-summary tab bar avoid firing on a spinner.

Components#

Every file in js/#

Twenty-four scripts plus a vendored SDK. The convention, stated in js/README.md, is classic scripts only: no ES modules, no bundling, no top-level const or let, no side-effect IIFEs, and each file linked at the exact position of the <script> block it replaced.

FileBytesWhat it owns
chapter-demos.js6,720Animates the nine mini-dashboards in the homepage chapters timeline. Values live in data-count, data-pct, --w and --h attributes so the markup is the source of truth. Honours reduced motion; if it never runs, CSS shows every end state
export-gate.js2,107Disables a tool's export and share buttons until a finished result exists, and re-disables on a new run. A lone loading, error, note or empty child does not count as a result. Wired to Keyword Overview only, as the reference implementation
kgap-vs.js2,075Gap Finder: keeps the two visible domain fields in sync with the single hidden [data-in] the backend reads as "you vs competitor"
mv-advtech.js14,864Advanced Technical Checks. One URL, four zero-cost checks run in parallel against /api/tools and rendered as one report. Carries its own what and why copy so the pre-run state explains itself
mv-blog.js239,357Blog Studio, a full multi-site blog CMS inside the app. Every call is scoped by an x-mv-site header and proxied to the standalone blog platform. Lazy-initialised on first open
mv-charts.js20,909The shared chart system over Chart.js. Exposes mvBarChart, mvHBarChart, mvAreaChart, mvLineChart, mvDonutChart and mvScatterChart; each returns the instance or null and never throws. Publishes MV_CHART_PALETTE and MV_CHART_SEMANTIC
mv-combined.js7,954Merged tools: running a group's primary view also runs its siblings with the same input. The sibling's .mv-results node is moved, not cloned, so canvases and handlers survive, and returned home before each re-run
mv-content-checks.js19,941Plagiarism Checker and Fact Checker. Score ring, verdict banner, stat tiles, evidence. The ring value is an SVG presentation attribute, never inline CSS
mv-export.js31,071The shared export engine for PDF, Excel, CSV and JSON. Extracts one structured model from the rendered result so there is no per-tool export code. PDF is real vector text via jsPDF and autoTable
mv-guide-rules.js39,313The Get Recommendations rule table, keyed by tool type. A rule's build() returns findings { sev, t, area }. Rules must fire only on evidence; an empty result draws no button. Also loadable in Node so tests/guide-rules.mjs can exercise every rule
mv-helpers.js5,348Utilities extracted verbatim from the monolith, including mvRelativeTime and mvDownloadCsv
mv-i18n-dict.<lang>.jses 2,949,060 · pt 2,919,872 · de 2,973,662 · fr 3,010,267 · ru 3,990,234Generated. One statement assigning window.MV_I18N, keyed by the English source string. Never edit by hand. Loaded lazily, only when a non-English language is first applied
mv-i18n-runtime.js11,742The shared translation runtime for every page except dashboard.html. Text pass, attribute pass, observer, and the language switcher widget. See Localization system
mv-library.js44,000Saved Work as a two-level file explorer: tool folders, a folder tree, breadcrumb navigation, grid and list layouts, cross-folder search and the result viewer. Filtering and ordering are done in SQL, not the browser
mv-merge-tabs.js5,640Injects a tab strip at the top of each view in a merge group, so merged tools keep their own views while sharing one nav entry
mv-modal.js16,499Accessible replacements for the native dialogs: mvAlert, mvConfirm and mvPrompt, all promise-returning. Default buttons are OK, Confirm and Cancel. Localises its own chrome at the source. Shared file: index, pricing, dashboard and admin all load it
mv-particles.js7,087A vanilla port of a React particles component, because there is no build step. Color comes from a CSS custom property on the mount so it follows the theme
mv-polyfills.js13,470Five guarded blocks lifted from the monolith. Defines the globals _esc, _tonePal and _pal that the top-level premium renderers reference by bare name. Do not reorder relative to the inline blocks
mv-psi.js31,081The PageSpeed Insights report renderer, shared by the free tool page and the dashboard view. MVPSI.render(el, data, opts) returns true if it rendered the rich report and false if the host should fall back
mv-reveal.js4,632Scroll reveal for cards. Engages only when IntersectionObserver exists and motion is allowed; if it never engages, cards are simply always visible
mv-site-chrome.js5,478Behavior for the shared public navbar: mobile hamburger, the collapsible free-tools submenu below 860px, and language-pill relabelling. See Centralized nav and footer
rec-panel.js22,868The Get Recommendations panel controller. Places an invitation under the result and an action in the tool's tab bar, runs staged loading, and calls /api/premium-ai
tool-explainer.js9,861Motion and interactivity for every .mvte explainer on the page, fully scoped so several can coexist. Demo values come from data attributes so each tool is self-describing
tool-samples.js467,002window.MV_TOOL_SAMPLES, real captured tool results used by the See example preview. Data, not UI, and deliberately excluded from the translation scanner
vendor/supabase-2.38.0.umd.js98,746The self-hosted Supabase SDK used by the sign-in page, with a CDN fallback

Three more scripts live at the repository root rather than in js/: chat-widget.js (the assistant, one engine on three surfaces), pwa-install.js (the install banner) and blog-embed.js (the third-party embed). chatbot.js is a deliberate no-op stub, explained in Repository structure. mobile.css used to sit beside them, orphaned; it was removed along with index-legacy.html, the only page that still linked it.

Every file in css/#

Twenty-four stylesheets. Same convention: plain <link> tags, no preprocessor, each linked at the original position of the <style> block it replaced so the cascade is unchanged.

FileBytesNamespace and role
tokens.css4,051The single source of truth for color, spacing, radius, shadow and type. Light :root, dark under [data-theme="dark"]. Loaded first
app.css105,939Dashboard chrome lifted from the first inline style block: the three-column shell, rail, sidebar, topbar, cards, view chrome. Re-declares :root with the dark palette and puts light behind [data-theme="light"]
legacy-mvx.css1,050,377The frozen .mvx stylesheet, 12,046 lines, lifted byte for byte. Holds the eleven forked token definitions the ratchet tracks. May only shrink
mvr.css44,883The .mvr-* result component system: quick preview, the premium frame layer, result primitives, table polish, search and tool panels, Article Writer typography, the action bar, and the credit confirmation modal
mvr-premium.css9,192An additive global premium layer loaded last in <head> so it wins over the legacy sheet without mutating it
site-chrome.css24,088The public navbar and footer, namespaced .mv-*. Authored light-first
chapter-demos.css20,723The nine homepage mini-dashboards, .cd-*. Index only. Each chapter gets its own layout on purpose; what they share is surface, type scale and motion
blog.css72,633Blog Studio, .mvblog-*. Documents three click-feedback layers: :active, :focus-visible, and a JS-set .is-busy
tool-explainer.css23,767The in-app explainer, all classes .mvte-* scoped under .mvte
rec-panel.css16,832The recommendations panel, .mvrec-*
psi.css23,194The PageSpeed report, scoped under .psi-report and single-theme light on purpose to match Google. Deliberately outside the design-lint targets so it can keep Google's exact palette
library.css13,231The Library activity view, .mvlib-*
library-saved.css18,023The "you already ran this" prompt and the saved-results list, .mvsv-*
responsive-analyzer.css24,068The Responsive Website Analyzer, .ra-*. Token-only. Forces [hidden] { display: none !important } inside its view so display: grid cannot beat the user-agent rule
image-tool.css27,069The Image Tool, .it-*. All processing is in-browser via Canvas
content-checks.css14,379Plagiarism and Fact Checker, .mvcc-*
content-decay.css3,049Content Decay Detector, .cd-* scoped under #view-contentdecay. Shares a prefix with chapter-demos.css but no class name collides and the two are never on one page
advtech.css8,730Advanced Technical Checks, .mvat-*
social.css12,214The connected-channels panel, .mv-soc-*. Brand colors live here on purpose, justified by being outside the ratchet so six real logo colors do not become a second token source
notifhub.css3,252Notifications and Alerts Hub, .nh-*. Kept out of the monolith specifically so the ratchet holds
merge-tabs.css1,050The merged-tool tab strip, .mvmg-*
tech-guide.css5,129The technical-SEO fix guide panel, .mvtg-*
gbp.css2,889The Google Business Profile details card, .gbp-*
dev-placeholder.css1,809The placeholder shown by tools announced in the nav but not yet built, .mvdev-*

Data flow#

Load order in dashboard.html#

Cascade order is source order, and several rules depend on it. The positions below are exact.

LineAssetWhy here
6js/mv-modal.jsNot deferred, before <title>, so mvAlert exists for anything that runs early
16Inline theme bootMust be in <head> above the first stylesheet, or the page paints one theme and snaps to the other
20 to 26tokens.css, app.css, responsive-analyzer.css, image-tool.css, library-saved.css, psi.css, tool-explainer.cssapp.css after tokens.css is what makes the dashboard dark-first
27 to 31tool-explainer.js, export-gate.js, rec-panel.css, rec-panel.js, kgap-vs.jsThe four deferred js/ files
28093 to 28100Chart.js 4.4.1, html2pdf 0.10.1, xlsx 0.18.5, jsPDF 2.5.1, jspdf-autotable 3.8.2, then mv-export.jsThe export engine must load after its vendors
28102 to 28103legacy-mvx.css, then mv-polyfills.jsThe polyfills sit immediately after the legacy sheet by design
42782 to 42793mv-helpers, mv-charts, tool-samples, mv-reveal, mv-guide-rules, mv-blog, mv-library, mv-merge-tabs, mv-advtech, mv-combined, mv-psi, mv-content-checksThe component block
59712 to 59724tech-guide, mvr, mvr-premium, blog, library, social, dev-placeholder, gbp, notifhub, merge-tabs, advtech, content-decay, content-checksThis is why mvr.css beats legacy-mvx.css: it is linked 31,600 lines later
61146chat-widget.jsLast

js/mv-i18n-dict.<lang>.js never appears as a static tag. Both translation runtimes inject it lazily.

Theme#

The storage key is localStorage('mv-theme') with values dark and light, and the attribute is data-theme on <html>. Light is the default: tokens.css declares light in :root and puts dark behind [data-theme="dark"].

The dashboard is the one genuine exception to light-first. css/app.css re-declares :root with the dark palette and loads after tokens.css, so it wins, and light is restored by a [data-theme="light"] block plus roughly ninety-five component-level rules. That is safe only because the dashboard's boot script always writes an explicit data-theme. The toggle's label shows the target state: it reads Light while dark and Dark while light.

tests/theme-lint.mjs checks three things per page: that it loads css/tokens.css, that its boot script is inline in <head> before the first stylesheet, and that it declares no competing color variables. It covers nineteen pages and currently passes all nineteen.

Charts and canvas#

Canvas cannot read CSS custom properties, so every color reaching Chart.js goes through resolveColor(), which resolves var(--x) via getComputedStyle with a fallback. A color that skips it renders invisible or black. isDark() reads data-theme from <html> then <body> and defaults to dark when neither is set.

Failure modes#

FailureSymptomCause
A renderer throwsThe result container keeps its spinner or shows the generic JSON fallbackmvRenderOld catches and logs [mv] renderer failed for <type>, then tries mvRenderAny. This class of silent failure is exactly what tests/smoke.mjs was written to catch
A js/ file is reorderedUndefined globals at render timeThe premium renderers reference _esc, _tonePal and _pal by bare name, defined in mv-polyfills.js
A stylesheet is movedThe wrong sheet winsCascade is source order. mvr.css only beats legacy-mvx.css because of where it is linked
A literal </script> inside a template literalSyntaxError: Unexpected end of input, whole page unclickableThis took production down once. tests/guard.mjs exists for it
A broken inline <script> blockEverything after it stops executingtests/chkblk.mjs compiles every inline block with vm.compileFunction and must report 0 broken
A \n search-and-replace matches nothingYour edit silently did not applydashboard.html and legal.html are CRLF. Use \r?\n
A shipped CSS or JS change does not appearCloudflare serves /js/* and /css/* with max-age=14400Bump the ?v= query string, and bump CACHE_NAME in sw.js
A local edit changes nothingwrangler pages dev dist serves dist/, not the repository rootCopy the edited file into dist/. See Local development

Gates to run before committing frontend work#

text
node tests/design-lint.mjs                # no tracked count may rise
node tests/chkblk.mjs dashboard.html      # 0 broken inline script blocks
node tests/guard.mjs dashboard.html       # no </script> inside a template literal
node tests/site-chrome-sync.mjs           # public chrome drift check
node tests/theme-lint.mjs                 # every page on the shared theme system
node tests/i18n-wire.mjs --check          # every page wired for translation

Add node tests/login-inline-sync.mjs if login.html changed, and run the full translation pipeline if any user-visible string changed. Every script is described in Verification scripts.

See also

Was this article helpful?