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