Viewing File: /usr/local/cpanel/base/frontend/meridian/_assets/master.html.tt
[%
# Copyright 2026 WebPros International, LLC
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.
%]
[%#
Meridian theme master.html.tt
Meridian variant of Phoenix's master template.
Uses the Meridian shell (goal-oriented navigation, command palette)
instead of the Phoenix AppShell.
React shell is always ON — no feature flag resolution needed.
All parameters are identical to Phoenix (see phoenix/_assets/master.html.tt).
%]
[%-
USE CacheBuster;
USE CPBranding;
USE VarCache;
USE Master;
USE Resource;
USE NVData;
USE Content_Includes;
USE UIAnalytics;
IF CPANEL.is_debug_mode_enabled();
SET mode = 'debug';
SET optimized = 0;
ELSE;
SET mode = 'release';
SET optimized = 1;
END;
# Meridian uses its own design system (Plus Jakarta Sans, JetBrains Mono)
# and does not need legacy Bootstrap/Open Sans stylesheets.
SET include_legacy_stylesheets = 0;
SET hide_header = 0 IF !hide_header.defined;
SET hide_license_warnings = 0 IF !hide_license_warnings.defined;
SET hide_quicklinks = 0 IF !hide_quicklinks.defined;
SET include_legacy_scripts = 0 IF !include_legacy_scripts.defined;
SET use_master_bootstrap = 1 IF !use_master_bootstrap.defined;
SET focus_feature_search = 0 IF !focus_feature_search.defined;
SET show_breadcrumb = 0 IF !show_breadcrumb.defined;
# Meridian theme: React shell is always ON
SET react_shell = 1;
SET is_react_app = 1 IF !is_react_app.defined;
# When react_shell AND is_react_app are ON, hide ALL legacy chrome.
# The React Meridian shell renders its own sidebar, header, heading, and footer.
IF react_shell AND is_react_app;
SET hide_header = 1 IF !hide_header || hide_header == 0;
SET hide_main_menu = 1 IF !hide_main_menu || hide_main_menu == 0;
SET hide_page_heading = 1;
END;
SET analytics_page_meta = {
product_feature => app_key,
product_feature_group => "unknown",
framework_js => "YUI",
framework_css => "",
};
IF !hide_page_heading.defined;
SET hide_page_heading = !app_key.defined && !page_title.defined;
END;
SET locale_attributes = CPANEL.locale_info;
varcache.set('directory_prefix',CPANEL.CPVAR.dprefix);
SET user = CPANEL.user;
SET documentation_url = "https://go.cpanel.net/cpaneldocsHome";
SET app_info = CPBranding.get_application_from_available_applications(varcache.available_applications, app_key);
IF app_info && app_info.group;
analytics_page_meta.product_feature_group = app_info.group;
END;
IF is_ng;
analytics_page_meta.framework_js = "Angular";
ELSIF use_master_bootstrap == 0 && include_legacy_scripts == 0;
analytics_page_meta.framework_js = "AngularJS";
END;
SET title = "cPanel";
IF page_title;
SET title = "cPanel - " _ page_title;
ELSIF app_key.defined;
SET title = "cPanel - " _ app_info.itemdesc;
ELSE;
SET title = "cPanel";
END;
SET page_title = page_title || app_info.itemdesc;
SET locale_query_string = "?locale=" _ locale.get_language_tag _ "&locale_optional=1&locale_revision=" _ calculate_magic_lex_mtime(locale.get_language_tag());
SET direction = locale_attributes.direction;
SET placement = direction == "rtl" ? "left" : "right";
SET stylesheets = [];
varcache.set('customizations', CPBranding.get_customizations("brand/cpanel-logo-white.svg", "brand/cpanel-logo-orange.svg", "brand/favicon.ico"));
SET display_cpanel_doclinks = ExpVar.expand('$display_cpanel_doclinks');
SET baseFile = ExpVar.expand('$basefile');
IF CPANEL.CPFLAGS.items('dev') == 1;
SET host_name = ExpVar.expand('$hostname');
SET main_server_ip = ExpVar.expand('$mainserverip');
SET main_ip = ExpVar.expand('$mainip');
END;
SET authorized_to_access = !feature.defined OR CPANEL.feature(feature);
SET cPanel_version = CPANEL.version();
SET domain = CPANEL.CPDATA.DNS;
-%]
<!DOCTYPE html>
<html lang="[% locale_attributes.locale %]" dir="[% direction %]">
<head>
<meta name="robots" content="noindex, nofollow" />
<title>[% title %]</title>
<meta charset="[% locale_attributes.encoding %]" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=1">
<meta name="theme-color" content="#f97316">
<meta name="mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="apple-touch-icon" href="[% varcache.directory_prefix %]assets/brand/cP_orange.png">
<meta name="apple-mobile-web-app-title" content="[% app_info.itemdesc %]">
<meta name="referrer" content="origin">
[%- IF meta -%]
[% meta %]
[% END -%]
[%IF varcache.customizations.favicon %]
<link rel="icon" href="[% varcache.customizations.favicon %]" type="image/x-icon" />
[% ELSE %]
<link rel="icon" href="[% varcache.directory_prefix %]assets/brand/favicon.ico" type="image/x-icon" />
[% END %]
<link rel="preload" href="[% theme_magic_url(calculate_mode_css_url('styles/fonts.css', optimized)) %]" as="style" id="custom-fonts-stylesheet" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="[% theme_magic_url(calculate_mode_css_url('styles/fonts.css', optimized)) %]" /></noscript>
[% IF react_shell %]
[%- SET shell_css_filename = direction == 'rtl' ? 'index.rtl.css' : 'index.css' -%]
<link rel="preload" href="[% theme_magic_url('_assets/meridian-shell/assets/' _ shell_css_filename) %]" as="style" />
[% END %]
[% IF load_meridian_shell_bundle %]
<link rel="modulepreload" href="[% theme_magic_url('_assets/meridian-shell/assets/index.js') %]" />
[% END %]
[%-
FOR source IN stylesheets;
SET url = theme_magic_url(calculate_mode_css_url(source, optimized));
-%]
<link rel="stylesheet" type="text/css" href="[% url %]" />
[%- END -%]
[%-
FOR source IN page_stylesheets;
IF is_ng;
SET url = theme_magic_url(source);
ELSE;
SET url = theme_magic_url(calculate_mode_css_url(source, optimized));
END;
-%]
<link rel="stylesheet" type="text/css" href="[% url %]" />
[%- END -%]
[%- IF embed_stylesheets %]
<style type="text/css">
[%- FOR source IN embed_stylesheets;
SET path = theme_magic_path(calculate_mode_css_url(source, optimized));
-%]
/* Embedding: [% path %] */
[% INSERT $path %]
[%- END -%]
</style>
[%- END -%]
[%- IF page_styles %]
[%- page_styles -%]
[% END -%]
<script>
(function(){
window.PAGE = {};
window.MASTER = {};
window.NVData = {};
window.thisTheme = [% CPANEL.CPDATA.RS.json() %];
})();
</script>
[%- IF page_js_first AND authorized_to_access -%]
[%- page_js_first -%]
[%- END -%]
<script type="text/javascript">
[% IF optimized %]
[% SET path = theme_magic_path("js/interfacereset.min.js") %]
[% ELSE %]
[% SET path = theme_magic_path("js/interfacereset.js") %]
[% END %]
[% INSERT $path %]
</script>
[%- IF page_js_top AND authorized_to_access -%]
[%- page_js_top -%]
[%- END -%]
[% Content_Includes.render("cpanel_jupiter_head.html.tt") %]
</head>
<body id="[% app_key %]" class="[% IF react_shell AND is_react_app %]meridian-shell[% ELSE %]cpanel yui-skin-sam cpanel_body[% END %]" data-app-key="[% app_key %]">
[%# Legacy CJT/web-component analytics loader: builds analyticsConfig
(which embeds loginUser => CPANEL.authuser) and emits the defining
<cp-ui-load-analytics> element. On Meridian (react_shell AND
is_react_app) the defining web-components bundle is never loaded, so
this is inert — but it is dead username-bearing markup and a latent
double-init tripwire if CJT is ever re-enabled. Gate it to the
non-React path. On the React path master builds cp_analytics_data
just before the INCLUDE below (see that SET), so the
cp_analytics_data.UUID read after the INCLUDE resolves and
react_shell_data's IF !cp_analytics_data fallback is a no-op. %]
[%- IF NOT (react_shell AND is_react_app) -%]
[%
SET cp_analytics_data = UIAnalytics.get_cpanel_analytics_data();
FOREACH key IN analytics_page_meta.keys.sort;
cp_analytics_data.$key = analytics_page_meta.$key;
END;
SET analyticsConfig = {
cpAnalyticsData => cp_analytics_data,
debugMode => CPANEL.is_debug_mode_enabled(),
loginUser => CPANEL.authuser,
isSandbox => CPANEL.is_sandbox(),
canTrackUserAnalytics => can_track_user_analytics,
isUserAnalyticsRequiredByLeika => is_user_analytics_required_by_leika,
};
%]
<cp-ui-load-analytics
analytics-config="[% analyticsConfig.json() | html %]"
>
</cp-ui-load-analytics>
[%- END -%]
[%- IF react_shell AND is_react_app -%]
[%# Build the analytics bundle in THIS (parent) scope BEFORE the INCLUDE
below so the cp_analytics_data.UUID read after the INCLUDE resolves.
A fresh `SET cp_analytics_data = ...` performed *inside* an INCLUDEd
template is localized and does NOT propagate back here (only nested
mutation of an already-existing hashref does). Building it here means
react_shell_data's `IF !cp_analytics_data` fallback is a no-op (one
get_cpanel_analytics_data call, not two) and reuses this hashref by
reference, so its framework_js="React" override is visible here too. %]
[%- SET cp_analytics_data = UIAnalytics.get_cpanel_analytics_data() -%]
[%# Same parent-scope-before-INCLUDE trick for initial_theme (the theme
the account was provisioned with; value semantics live in
getInitialThemeFromPage()'s docs, @cpanel/utils/page-data), which BOTH
this template and react_shell_data.html.tt need. Reading it HERE makes
react_shell_data's `IF !cp_initial_theme_value` fallback a no-op —
one fetch per page load: it emits window.PAGE.shell.initialTheme and
the onboarding block below reuses the variable for its gates. The 4th
arg selects the cPanel (not WHM) NVData store — not a namespace; the
on-disk key is plain `initial_theme`. DUCKS-6711. -%]
[%- SET cp_initial_theme_value = NVData.get('initial_theme', undef, 0, 'cpanel') -%]
<script>
if (typeof window.PAGE === "undefined") window.PAGE = {};
Object.assign(window.PAGE, {
[% INCLUDE '_assets/react_shell_data.html.tt' %]
});
// Meridian: set theme in shell data
if (window.PAGE.shell) {
window.PAGE.shell.theme = 'meridian';
[%- IF cp_analytics_data.UUID -%]
// Stable per-account analytics identifier — the same UUID
// Jupiter passes to mixpanel.identify(), so a user's events
// merge onto one Mixpanel profile across both themes. Absent
// for root WHM sessions (no per-account UUID).
window.PAGE.shell.analyticsUuid = [% cp_analytics_data.UUID.json() %];
[%- END -%]
}
[%# Read onboarding state via the NVData plugin. Use the exact
variable name bound by `USE NVData` above — `NVData`, NOT
`NVDATA`. TT variable names are case-sensitive: `.get()` on the
undefined `NVDATA` silently returns an empty string, which the
falsy ternary below emitted as `null` — routing a user who had
already completed onboarding back into the wizard, which then
bounced them to the dashboard (the "onboarding flash",
DUCKS-6183). The same typo also silently broke resume/dismissals. -%]
[%- SET mob_status_val = NVData.get('meridian_onboarding_status');
SET mob_resume_val = NVData.get('meridian_onboarding_resume');
SET mob_dismissals_val = NVData.get('meridian_onboarding_dismissals'); -%]
[%# Docroot-content probe. An account with no
meridian_onboarding_status has never resolved onboarding, and we
can't yet tell "never onboarded" apart from "already has a
website, just never used Meridian onboarding" — a Jupiter
switcher with a live site, but equally a brand-new native
Meridian account whose content was migrated in over FTP/rsync or
pre-provisioned by the host. Resolve it here, synchronously, so
the client-side redirect gate (useFirstLoginRedirect) never has
to guess or flash the dashboard before redirecting, and so the
client probes (which only ever see builder-managed/published
sites) aren't the only signal. Cohort: ANY account whose
onboarding status is unresolved AND which has no in-progress
journey — native and switcher alike. DUCKS-6711, DUCKS-6712. -%]
[%# The !mob_resume_val half is the resume guard: a journey must
never be terminated by content the journey itself created (an
in-place WordPress install or a custom-journey file upload fills
the docroot mid-wizard; without this guard the next page load
reads content and writes a terminal not_applicable status,
destroying a resumable journey). It also removes the probe cost
from every mid-wizard reload. Accepted flip side: a STALE resume
blob over content that predates the journey also suppresses the
probe (raw truthiness — TT2 cannot judge blob freshness), so
that account resumes the wizard and exits via its own Skip
instead of being silently resolved. DUCKS-6712. -%]
[%# Tri-state, carried as the literal JSON token that gets emitted:
'true'/'false' only for a POSITIVELY resolved probe, 'null' for
every unknown (check not needed, API error, missing entry or
flag). Clients treat 'false' as authoritative and skip their own
probes, so collapsing an unknown into 'false' would send an
account that has a site into the wizard. -%]
[%# Deliberately NOT skipped for switch-to-user (impersonated)
sessions. loginType is computed inline in the emitted object
below out of a fragile CPRESELLERSESSION/CPRESELLER/HTTP_REFERER
ENV expression; gating the probe on it would mean hoisting that
expression above this block and keeping two consumers of it in
sync, to save ~1s only on the narrow "admin impersonating an
unresolved account that also has no resume blob" path. Emitting
the identical signal for every session type also keeps the
client gates from silently diverging by session. Don't
"optimize" this. -%]
[%# Known limitation: only the PRIMARY domain's docroot is probed,
and the backend _is_empty_directory skips nested sub-docroots, so
a switcher whose only site lives on an addon domain reads an
authoritative 'false'. Accepted. -%]
[%# Residual cost: an unresolved account with an EMPTY docroot and no
resume blob still pays the probe on every page load, until it
finishes or skips onboarding. Unresolvable server-side (there is
nothing to persist about "still empty"); the 'true' half is
closed by useResolveOnboardingStatusFromDocroot, which persists
the terminal status on the first load that sees content. -%]
[%- SET mob_needs_docroot_check = !mob_status_val && !mob_resume_val;
SET mob_docroot_has_content = 'null'; -%]
[%- IF mob_needs_docroot_check; -%]
[%# USE binds are localized to their template — react_shell_data's
`USE Uapi` (an INCLUDE) is NOT visible here, and TT2 silently
yields undef for method calls on the unbound name instead of
throwing, so without this line the probe "succeeds" as
no-website for every account. Bind it in THIS scope. -%]
[%- USE Uapi; -%]
[%- TRY; -%]
[%# Sitejet::disk_quota_check is the lightweight probe —
same authoritative is_docroot_empty as
get_all_user_sitejet_info's quota block at roughly half
the cost (no publish/CMS/restore-point probing). Its
data is a hash KEYED BY DOMAIN, and domains contain
dots, so dot-access can't reach the entry — item() is
required. -%]
[%- SET mob_quota_entry = Uapi.exec('Sitejet', 'disk_quota_check', {
domain = CPANEL.CPDATA.DNS
}).data.item(CPANEL.CPDATA.DNS); -%]
[%# Only a defined is_docroot_empty resolves the probe. An
absent entry or flag is unknown, not content — same
semantics as sitejetDocRootHasContent() returning null
client-side. -%]
[%- IF mob_quota_entry && mob_quota_entry.is_docroot_empty.defined; -%]
[%- SET mob_docroot_has_content =
mob_quota_entry.is_docroot_empty ? 'false' : 'true'; -%]
[%- END; -%]
[%- CATCH; -%]
[%# An API error leaves the tri-state at 'null' so clients
run their own existing-site checks rather than trusting
an unverified "no website". -%]
[%- END; -%]
[%- END; -%]
// Onboarding V3 config — injected for the step composer
// Onboarding-specific bag: only the keys that don't live anywhere
// else. features/roles/services moved into window.PAGE.features and
// window.PAGE.capabilities (see react_shell_data.html.tt) so every
// hub reads the same data from one source.
window.PAGE.onboarding = {
enabled: true,
flow: 'default',
upsells: 'none',
[%# Analytics consent gating (Leika `global.analytics_consent.enable`)
is NOT an onboarding concern — it now lives in
window.PAGE.analytics_info.consentRequired (see
react_shell_data.html.tt), read via getAnalyticsConsentRequired().
DUCKS-6290. %]
status: [% mob_status_val ? mob_status_val.json() : 'null' %],
resume: [% mob_resume_val ? mob_resume_val.json() : 'null' %],
dismissals: [% mob_dismissals_val ? mob_dismissals_val.json() : 'null' %],
[%# docrootHasContent (DUCKS-6711, renamed + widened in
DUCKS-6712): see getDocrootHasContentFromPage()'s consumers
in useFirstLoginRedirect, useResolveOnboardingStatusFromDocroot,
OnboardingHubV3 and DashboardHub. null
means "unknown" — an account already resolved to a non-null
status, an account with an in-progress journey, or a probe
that failed or didn't resolve; in all cases the client falls
back to its own checks. The provisioning theme itself is NOT
re-emitted here — it lives at window.PAGE.shell.initialTheme
(react_shell_data.html.tt). %]
docrootHasContent: [% mob_docroot_has_content %],
[%# CPRESELLER*/HTTP_REFERER only fire on the FIRST page load of
an admin session, so this reads 'direct' after a reload or a
Jupiter theme switch. Never gate on the raw string — use
isSwitchToUserSession() in @cpanel/utils/page-data, which
also honours a per-tab flag. %]
loginType: [%
CPANEL.ENV.CPRESELLERSESSION.defined
|| CPANEL.ENV.item("CPRESELLER")
|| CPANEL.ENV.HTTP_REFERER.match(':208[67]')
? '"switch-to-user"' : '"direct"'
%]
};
</script>
[%- END -%]
[%# Shell CSS is needed on ALL React pages for MeridianShell layout (grid, sidebar, header).
Shell JS is only needed on the homepage (it mounts the hub SPA with Dashboard routing).
shell_css_filename was computed in <head> and reused here. %]
[%- IF react_shell -%]
<link rel="stylesheet" type="text/css" href="[% theme_magic_url('_assets/meridian-shell/assets/' _ shell_css_filename) %]" />
[%- END -%]
[%- IF load_meridian_shell_bundle -%]
<script type="module" src="[% theme_magic_url('_assets/meridian-shell/assets/index.js') %]"></script>
[%- END -%]
[%
PROCESS _assets/master_content.html.tt;
%]
[% IF varcache.customizations.stylesheet %]
<style id="style-customizations">
[% varcache.customizations.stylesheet %]
</style>
[% END -%]
[%- IF page_init_js AND authorized_to_access -%]
[%- page_init_js -%]
[%- END -%]
[%- IF page_scripts AND authorized_to_access;
FOR source IN page_scripts;
IF is_ng;
SET url = theme_magic_url(source);
ELSE;
SET url = theme_magic_url(calculate_mode_js_url(source, optimized));
END;
-%]
<script type="text/javascript" src="[% url _ locale_query_string %]"></script>
[% END;
END -%]
[%- IF embed_scripts AND authorized_to_access %]
<script type="text/javascript">
[%- FOR source IN embed_scripts;
SET path = theme_magic_path(calculate_mode_js_url(source, optimized));
%]
/* Embedding: [% path %] */
[% INSERT $path %]
[%
SET lex_path = CPANEL.get_js_lex_app_full_path(path);
IF lex_path;
%]
/* Embedding: [% lex_path %] */
[% INSERT $lex_path %]
[%- END %]
[%- END -%]
</script>
[%- END %]
[%- IF page_js AND authorized_to_access -%]
[%- page_js -%]
[%- END -%]
[% IF is_ng %]
<script type="text/javascript" src="[% theme_magic_url('_assets/breadcrumb.js') %]"></script>
[% END %]
</body>
</html>
Back to Directory