Cloudflare Pro

PHP

Advanced Cloudflare DNS management extension for Plesk with DNS record synchronization, zone management, proxy control, SSL automation, and seamless Cloudflare integration.

Stars
16
Forks
1
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
docs/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cloudflare Pro for Plesk | Documentation</title>
  <meta name="description" content="Complete documentation for Cloudflare Pro, a Plesk extension for syncing Plesk DNS records with Cloudflare using scoped tokens, record comparison, API logs, settings, and background sync jobs.">
  <meta name="keywords" content="Cloudflare Pro Plesk, Plesk Cloudflare extension, DNS sync Plesk, Cloudflare DNS Plesk, Plesk DNS manager">
  <meta name="author" content="Ghost Compiler">
  <meta name="robots" content="index, follow, max-image-preview:large">
  <meta name="application-name" content="Cloudflare Pro">
  <link rel="canonical" href="https://ghostcompiler.github.io/cloudflare-pro/">
  <link rel="icon" href="../htdocs/images/cloudflare-icon-32.png">
  <link rel="apple-touch-icon" href="../htdocs/images/cloudflare-icon-32.png">
  <meta name="theme-color" content="#f38020">

  <meta property="og:type" content="website">
  <meta property="og:site_name" content="Cloudflare Pro">
  <meta property="og:title" content="Cloudflare Pro for Plesk">
  <meta property="og:description" content="Sync Plesk DNS records with Cloudflare using token-scoped access, API logs, per-user settings, and background sync jobs.">
  <meta property="og:url" content="https://ghostcompiler.github.io/cloudflare-pro/">
  <meta property="og:image" content="https://res.cloudinary.com/djgvfl1tv/image/upload/v1780666786/favicon_oqj6jr.svg">
  <meta property="og:image:alt" content="Cloudflare Pro domain dashboard in Plesk dark theme">
  <meta property="og:locale" content="en_US">

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="Cloudflare Pro for Plesk">
  <meta name="twitter:description" content="Import, export, compare, and sync Plesk DNS records with Cloudflare from a Plesk extension.">
  <meta name="twitter:image" content="https://res.cloudinary.com/djgvfl1tv/image/upload/v1780666786/favicon_oqj6jr.svg">
  <meta name="twitter:image:alt" content="Cloudflare Pro domain dashboard in Plesk dark theme">

  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "SoftwareApplication",
    "name": "Cloudflare Pro for Plesk",
    "applicationCategory": "DeveloperApplication",
    "operatingSystem": "Linux, Plesk",
    "description": "A Plesk extension for syncing Plesk DNS records with Cloudflare using scoped tokens, record comparison, API logs, settings, and background sync jobs.",
    "url": "https://ghostcompiler.github.io/cloudflare-pro/",
    "downloadUrl": "https://github.com/ghostcompiler/cloudflare-pro/releases/download/latest/cloudflare-pro.zip",
    "softwareVersion": "1.0.5",
    "author": {
      "@type": "Organization",
      "name": "Ghost Compiler",
      "url": "https://github.com/ghostcompiler"
    },
    "codeRepository": "https://github.com/ghostcompiler/cloudflare-pro",
    "image": "https://ghostcompiler.github.io/cloudflare-pro/screenshots/light_domain.png",
    "offers": {
      "@type": "Offer",
      "price": "0",
      "priceCurrency": "USD"
    }
  }
  </script>

  <style>
    :root {
      --bg: #f7f9fc;
      --ink: #172033;
      --muted: #617083;
      --line: #dce4ee;
      --panel: #fff;
      --panel-2: #eef5f9;
      --accent: #f38020;
      --accent-2: #2563eb;
      --blue: #28aade;
      --green: #528b2e;
      --code: #111827;
      --code-ink: #d1fae5;
      --shadow: 0 18px 48px rgba(15, 23, 42, .12);
    }

    * {
      box-sizing: border-box;
    }

    html {
      scroll-behavior: smooth;
    }

    body {
      margin: 0;
      color: var(--ink);
      background: var(--bg);
      font: 16px/1.65 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }

    a {
      color: var(--accent-2);
      text-decoration: none;
    }

    a:hover {
      text-decoration: underline;
    }

    img {
      display: block;
      max-width: 100%;
    }

    code,
    pre {
      font-family: Menlo, Consolas, "Liberation Mono", monospace;
    }

    .topbar {
      position: sticky;
      top: 0;
      z-index: 20;
      border-bottom: 1px solid rgba(220, 228, 238, .88);
      background: rgba(247, 249, 252, .94);
      backdrop-filter: blur(14px);
    }

    .nav {
      width: min(1180px, calc(100% - 32px));
      min-height: 64px;
      margin: 0 auto;
      display: flex;
      gap: 18px;
      align-items: center;
      justify-content: space-between;
    }

    .brand {
      display: flex;
      gap: 10px;
      align-items: center;
      min-width: 0;
      color: var(--ink);
      font-weight: 900;
      white-space: nowrap;
    }

    .brand img {
      width: 34px;
      height: 34px;
      object-fit: contain;
    }

    .nav-links {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
      justify-content: flex-end;
      font-size: 14px;
      font-weight: 800;
    }

    .nav-links a {
      color: #334155;
    }

    .button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 42px;
      padding: 9px 15px;
      border: 1px solid var(--line);
      border-radius: 6px;
      color: var(--ink);
      background: #fff;
      font-weight: 850;
      text-decoration: none;
      white-space: nowrap;
    }

    .button:hover {
      border-color: #aebdca;
      text-decoration: none;
    }

    .button.primary {
      border-color: var(--accent);
      color: #fff;
      background: var(--accent);
    }

    .hero {
      position: relative;
      min-height: 690px;
      display: grid;
      align-items: end;
      overflow: hidden;
      color: #fff;
      background: #111827;
    }

    .hero::before {
      content: "";
      position: absolute;
      inset: 0;
      background:
        linear-gradient(90deg, rgba(6, 15, 27, .96) 0%, rgba(6, 15, 27, .78) 44%, rgba(6, 15, 27, .20) 100%),
        url("screenshots/light_domain.png") center / cover no-repeat;
      transform: scale(1.02);
    }

    .hero-inner {
      position: relative;
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
      padding: 110px 0 80px;
    }

    .eyebrow {
      margin: 0 0 12px;
      color: #fed7aa;
      font-size: 13px;
      font-weight: 950;
      letter-spacing: .08em;
      text-transform: uppercase;
    }

    h1,
    h2,
    h3 {
      line-height: 1.12;
      letter-spacing: 0;
    }

    h1 {
      max-width: 790px;
      margin: 0;
      font-size: clamp(42px, 8vw, 82px);
      font-weight: 950;
    }

    h2 {
      margin: 0 0 14px;
      font-size: clamp(30px, 4vw, 46px);
      font-weight: 900;
    }

    h3 {
      margin: 0 0 10px;
      font-size: 22px;
      font-weight: 850;
    }

    .hero-copy {
      max-width: 760px;
      margin: 22px 0 0;
      color: #d8e2ec;
      font-size: 20px;
    }

    .hero-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 12px;
      margin-top: 30px;
    }

    .hero-actions .button:not(.primary) {
      color: #fff;
      border-color: rgba(255, 255, 255, .28);
      background: rgba(255, 255, 255, .10);
    }

    .stats,
    .grid,
    .feature-grid,
    .shot-grid,
    .option-grid {
      display: grid;
      gap: 16px;
    }

    .stats {
      grid-template-columns: repeat(4, minmax(0, 1fr));
      margin-top: 40px;
    }

    .stat,
    .card,
    .option,
    .shot {
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--panel);
      box-shadow: 0 1px 0 rgba(15, 23, 42, .03);
    }

    .stat {
      padding: 18px;
      color: #fff;
      border-color: rgba(255, 255, 255, .2);
      background: rgba(255, 255, 255, .10);
    }

    .stat strong {
      display: block;
      font-size: 30px;
      line-height: 1;
    }

    .stat span {
      display: block;
      margin-top: 8px;
      color: #d8e2ec;
      font-size: 13px;
      font-weight: 800;
      text-transform: uppercase;
    }

    .section {
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
      padding: 74px 0;
    }

    .section.lead {
      padding-top: 64px;
    }

    .section-copy {
      max-width: 820px;
      margin: 0 0 26px;
      color: var(--muted);
      font-size: 18px;
    }

    .grid.two {
      grid-template-columns: 1.05fr .95fr;
      align-items: start;
    }

    .feature-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .option-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .card,
    .option {
      padding: 22px;
    }

    .card p,
    .option p {
      margin: 0;
      color: var(--muted);
    }

    .card ul,
    .option ul {
      margin: 12px 0 0;
      padding-left: 20px;
      color: var(--muted);
    }

    .doc-card a {
      display: inline-block;
      margin-bottom: 8px;
      font-weight: 900;
    }

    .kicker {
      margin: 0 0 8px;
      color: var(--accent);
      font-size: 12px;
      font-weight: 950;
      letter-spacing: .08em;
      text-transform: uppercase;
    }

    .showcase {
      width: 100%;
      padding: 86px 0;
      color: #f9fafb;
      background: #111827;
    }

    .showcase-inner {
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
    }

    .showcase h2 {
      max-width: 760px;
      color: #fff;
      font-size: clamp(34px, 5vw, 62px);
      line-height: 1.05;
    }

    .showcase .section-copy {
      color: #b8c4d1;
    }

    .slider-shell {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 260px;
      gap: 22px;
      align-items: start;
      margin-top: 34px;
    }

    .compare {
      position: relative;
      overflow: hidden;
      border: 1px solid rgba(255, 255, 255, .16);
      border-radius: 8px;
      background: #0b1220;
      box-shadow: 0 24px 64px rgba(0, 0, 0, .34);
      aspect-ratio: 3420 / 1908;
    }

    .compare img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      user-select: none;
      pointer-events: none;
    }

    .compare .light-img {
      clip-path: inset(0 calc(100% - var(--split, 50%)) 0 0);
    }

    .compare input[type="range"] {
      position: absolute;
      inset: auto 18px 18px;
      z-index: 4;
      width: calc(100% - 36px);
      accent-color: var(--accent);
    }

    .split-line {
      position: absolute;
      top: 0;
      bottom: 0;
      left: var(--split, 50%);
      z-index: 3;
      width: 2px;
      background: var(--accent);
      box-shadow: 0 0 0 1px rgba(17, 24, 39, .55);
    }

    .split-line::after {
      content: "Light | Dark";
      position: absolute;
      top: 18px;
      left: 50%;
      transform: translateX(-50%);
      padding: 7px 10px;
      border-radius: 999px;
      color: #3b1d06;
      background: #fed7aa;
      font-size: 12px;
      font-weight: 900;
      white-space: nowrap;
    }

    .thumbs {
      display: grid;
      gap: 10px;
    }

    .thumbs button {
      width: 100%;
      min-height: 48px;
      padding: 11px 13px;
      border: 1px solid rgba(255, 255, 255, .14);
      border-radius: 6px;
      color: #dbe7f4;
      background: rgba(255, 255, 255, .08);
      font: inherit;
      font-weight: 800;
      text-align: left;
      cursor: pointer;
    }

    .thumbs button.active {
      color: #431407;
      border-color: var(--accent);
      background: #fed7aa;
    }

    .steps {
      counter-reset: step;
      display: grid;
      gap: 14px;
      margin: 24px 0 0;
      padding: 0;
      list-style: none;
    }

    .steps li {
      counter-increment: step;
      position: relative;
      padding: 18px 18px 18px 58px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: #fff;
    }

    .steps li::before {
      content: counter(step);
      position: absolute;
      left: 18px;
      top: 18px;
      width: 28px;
      height: 28px;
      display: grid;
      place-items: center;
      border-radius: 999px;
      color: #fff;
      background: var(--accent);
      font-weight: 900;
    }

    pre {
      position: relative;
      overflow: auto;
      margin: 16px 0 0;
      padding: 16px;
      border-radius: 8px;
      color: var(--code-ink);
      background: var(--code);
      font-size: 13px;
      line-height: 1.55;
    }

    .copy-code {
      position: absolute;
      top: 10px;
      right: 10px;
      z-index: 2;
      min-height: 30px;
      padding: 5px 9px;
      border: 1px solid rgba(255, 255, 255, .16);
      border-radius: 6px;
      color: #dbeafe;
      background: rgba(15, 23, 42, .86);
      font: inherit;
      font-size: 12px;
      font-weight: 850;
      cursor: pointer;
    }

    .copy-code:hover {
      border-color: rgba(255, 255, 255, .34);
      background: rgba(30, 41, 59, .94);
    }

    .note {
      padding: 18px;
      border-left: 4px solid var(--accent);
      border-radius: 6px;
      background: var(--panel-2);
      color: #334155;
    }

    .footer {
      border-top: 1px solid var(--line);
      background: #fff;
    }

    .footer-inner {
      width: min(1180px, calc(100% - 32px));
      margin: 0 auto;
      padding: 34px 0;
      color: var(--muted);
      display: flex;
      gap: 18px;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
    }

    @media (max-width: 900px) {
      .nav {
        align-items: flex-start;
        flex-direction: column;
        padding: 12px 0;
      }

      .stats,
      .grid.two,
      .feature-grid,
      .option-grid,
      .slider-shell {
        grid-template-columns: 1fr;
      }

      .hero {
        min-height: 620px;
      }
    }
  </style>
</head>
<body>
  <header class="topbar">
    <nav class="nav" aria-label="Main navigation">
      <a class="brand" href="#top">
        <img src="https://res.cloudinary.com/djgvfl1tv/image/upload/v1780666786/favicon_oqj6jr.svg" alt="Cloudflare Pro icon">
        <span>Cloudflare Pro</span>
      </a>
      <div class="nav-links">
        <a href="#docs">Docs</a>
        <a href="#features">Features</a>
        <a href="#options">Options</a>
        <a href="#screenshots">Screenshots</a>
        <a href="#install">Install</a>
        <a href="https://github.com/ghostcompiler/cloudflare-pro">GitHub</a>
      </div>
    </nav>
  </header>

  <main id="top">
    <section class="hero">
      <div class="hero-inner">
        <p class="eyebrow">Plesk extension for Cloudflare DNS</p>
        <h1>Cloudflare Pro for Plesk</h1>
        <p class="hero-copy">Sync Plesk DNS records with Cloudflare using token-scoped access, record comparison, API logs, per-user settings, and background sync jobs.</p>
        <div class="hero-actions">
          <a class="button primary" href="#install">Install latest package</a>
          <a class="button" href="#options">View all options</a>
          <a class="button" href="https://github.com/ghostcompiler/cloudflare-pro">Open repository</a>
        </div>
        <div class="stats" aria-label="Key capabilities">
          <div class="stat"><strong>DNS</strong><span>Import, export, sync</span></div>
          <div class="stat"><strong>Tokens</strong><span>Per-user access</span></div>
          <div class="stat"><strong>Logs</strong><span>Cloudflare API calls</span></div>
          <div class="stat"><strong>Jobs</strong><span>Background sync</span></div>
        </div>
      </div>
    </section>

    <section class="section lead" id="overview">
      <div class="grid two">
        <div>
          <p class="kicker">Overview</p>
          <h2>Built for Cloudflare DNS operations inside Plesk.</h2>
          <p class="section-copy">Cloudflare Pro links accessible Plesk domains to matching Cloudflare zones, compares local and remote DNS records, and exposes safe actions for import, export, sync, delete, proxy toggles, and troubleshooting logs.</p>
          <p class="section-copy">The extension keeps Cloudflare tokens scoped to the logged-in Plesk user, stores settings in SQLite, and uses queued sync jobs so larger zones can run with visible progress instead of long blocking requests.</p>
        </div>
        <div class="card">
          <h3>Typical workflows</h3>
          <ul>
            <li>Push Plesk DNS records into a Cloudflare zone.</li>
            <li>Pull Cloudflare DNS records back into Plesk.</li>
            <li>Compare mismatched records before deciding what to keep.</li>
            <li>Inspect Cloudflare API request and response details while debugging.</li>
          </ul>
        </div>
      </div>
    </section>

    <section class="section" id="docs">
      <p class="kicker">Documentation</p>
      <h2>Operator and developer docs.</h2>
      <p class="section-copy">The documentation set covers installation, extension architecture, controller/API behavior, and security notes for token handling, logging, and permissions.</p>
      <div class="feature-grid">
        <div class="card doc-card"><a href="INSTALL.md">Installation</a><p>Requirements, install commands, local package builds, first run, and troubleshooting logs.</p></div>
        <div class="card doc-card"><a href="ARCHITECTURE.md">Architecture</a><p>Extension layout, repositories, sync flow, event hooks, storage, and Plesk integration details.</p></div>
        <div class="card doc-card"><a href="API.md">API</a><p>Controller actions, Cloudflare routes, sync jobs, tokens, logs, settings, and record actions.</p></div>
        <div class="card doc-card"><a href="SECURITY.md">Security</a><p>Token storage, owner scoping, permissions, request logging, uninstall cleanup, and operational notes.</p></div>
      </div>
    </section>

    <section class="section" id="features">
      <p class="kicker">Features</p>
      <h2>Everything exposed by the extension.</h2>
      <p class="section-copy">Cloudflare Pro is organized around Domains, record comparison, Tokens, API Logs, Settings, and About. Each area is built for fast scanning and repeated DNS operations from the Plesk panel.</p>
      <div class="feature-grid">
        <div class="card"><h3>Domain discovery</h3><p>Finds Cloudflare zones that match accessible Plesk domains and stores linked zone metadata per user.</p></div>
        <div class="card"><h3>Record comparison</h3><p>Shows local-only, Cloudflare-only, mismatched, and synced records with search, pagination, sorting, and bulk selection.</p></div>
        <div class="card"><h3>Import and export</h3><p>Pull Cloudflare DNS records into Plesk or push Plesk DNS records to Cloudflare using queued background jobs.</p></div>
        <div class="card"><h3>Single record actions</h3><p>Select DNS records and run Push, Pull, or Delete only for the records that need attention.</p></div>
        <div class="card"><h3>Proxy controls</h3><p>Toggle Cloudflare proxy state for supported A, AAAA, and CNAME records.</p></div>
        <div class="card"><h3>Token storage</h3><p>Add, validate, edit, and delete Cloudflare API tokens without exposing another user's credentials.</p></div>
        <div class="card"><h3>API logs</h3><p>Search, paginate, view, copy, and remove Cloudflare API request and response logs.</p></div>
        <div class="card"><h3>Settings</h3><p>Control autosync, token validation, API logging, safe delete cleanup, subdomain www companions, and proxy defaults per user.</p></div>
        <div class="card"><h3>Autosync</h3><p>Use Plesk events to push supported DNS changes to Cloudflare when autosync is enabled.</p></div>
      </div>
    </section>

    <section class="section" id="options">
      <p class="kicker">Option reference</p>
      <h2>Every option available in the UI.</h2>
      <div class="option-grid">
        <div class="option">
          <h3>Domains tab</h3>
          <ul>
            <li><strong>View</strong>: opens the DNS record comparison page.</li>
            <li><strong>Export</strong>: pushes local Plesk records to Cloudflare.</li>
            <li><strong>Import</strong>: pulls Cloudflare records into Plesk.</li>
            <li><strong>Sync</strong>: processes DNS records through a persisted sync job.</li>
            <li><strong>Auto Sync</strong>: enables event-based pushing for that linked domain.</li>
          </ul>
        </div>
        <div class="option">
          <h3>Record page</h3>
          <ul>
            <li><strong>Back</strong>: returns to Domains.</li>
            <li><strong>Import / Export / Sync All</strong>: runs bulk domain record operations.</li>
            <li><strong>Push / Pull / Delete</strong>: appears after selecting records.</li>
            <li><strong>Search</strong>: searches across type, name, status, and values.</li>
            <li><strong>Proxy</strong>: changes Cloudflare proxy state where supported.</li>
          </ul>
        </div>
        <div class="option">
          <h3>Tokens tab</h3>
          <ul>
            <li><strong>Add Token</strong>: opens the credential drawer.</li>
            <li><strong>Validate</strong>: calls Cloudflare token verification.</li>
            <li><strong>Edit</strong>: updates the token name or token value.</li>
            <li><strong>Delete</strong>: removes the token and linked domain rows.</li>
          </ul>
        </div>
        <div class="option">
          <h3>API Logs tab</h3>
          <ul>
            <li><strong>Search logs</strong>: filters logged Cloudflare requests.</li>
            <li><strong>View</strong>: opens request and response details.</li>
            <li><strong>Copy</strong>: copies request and response JSON.</li>
            <li><strong>Remove Logs</strong>: clears stored logs for the current user.</li>
          </ul>
        </div>
        <div class="option">
          <h3>Settings tab</h3>
          <ul>
            <li><strong>Enable Autosync</strong>: allows automatic DNS pushes.</li>
            <li><strong>Validate token before sync</strong>: verifies token status before sync jobs.</li>
            <li><strong>Log Cloudflare API calls</strong>: stores explicit Cloudflare API request logs.</li>
            <li><strong>Create www record for subdomains</strong>: creates matching Cloudflare-only www records during subdomain autosync and removes the companion record when that subdomain is deleted.</li>
            <li><strong>Remove records automatically on domain delete</strong>: removes concrete deleted child hostnames only; ambiguous apex delete events are skipped to protect the zone.</li>
            <li><strong>Proxy defaults</strong>: controls default proxy state for new A, AAAA, and CNAME records.</li>
          </ul>
        </div>
        <div class="option">
          <h3>Supported DNS records</h3>
          <ul>
            <li>A, AAAA, CNAME, MX, TXT, PTR, CAA, DS, DNSKEY, TLSA, and SRV.</li>
            <li>TLSA and SRV use Cloudflare structured data fields.</li>
            <li>Cloudflare proxy controls are limited to A, AAAA, and CNAME records.</li>
          </ul>
        </div>
      </div>
    </section>

    <section class="showcase" id="screenshots">
      <div class="showcase-inner">
        <p class="kicker">Screenshots</p>
        <h2>Light and dark theme screenshots.</h2>
        <p class="section-copy">Pick a screen, then drag the handle to compare the Plesk light and dark appearances.</p>
        <div class="slider-shell">
          <div class="compare" id="compare" style="--split: 50%">
            <img id="darkShot" src="screenshots/dark_domain.png" alt="Cloudflare Pro domains dark theme">
            <img id="lightShot" class="light-img" src="screenshots/light_domain.png" alt="Cloudflare Pro domains light theme">
            <div class="split-line" aria-hidden="true"></div>
            <input id="themeRange" type="range" min="0" max="100" value="50" aria-label="Compare light and dark theme screenshots">
          </div>
          <div class="thumbs" aria-label="Screenshot selector">
            <button class="active" type="button" data-view="domain" data-label="Domains">Domains</button>
            <button type="button" data-view="records" data-label="Records">Records</button>
            <button type="button" data-view="tokens" data-label="Tokens">Tokens</button>
            <button type="button" data-view="addtoken" data-label="Add Token">Add Token</button>
            <button type="button" data-view="logs" data-label="API Logs">API Logs</button>
            <button type="button" data-view="settings" data-label="Settings">Settings</button>
          </div>
        </div>
      </div>
    </section>

    <section class="section" id="install">
      <p class="kicker">Install</p>
      <h2>Install or build the extension.</h2>
      <p class="section-copy">Use the rolling latest package for most installations, or build the ZIP locally from the repository.</p>
      <pre><code>plesk bin extension --install-url https://github.com/ghostcompiler/cloudflare-pro/releases/download/latest/cloudflare-pro.zip</code></pre>
      <pre><code>sh packaging/build.sh
plesk bin extension --install cloudflare-pro-1.0.5.zip</code></pre>
      <ol class="steps">
        <li>Open the affected service plan or subscription in Plesk.</li>
        <li>Enable Cloudflare Pro access where needed.</li>
        <li>Add a Cloudflare API token with Zone Read, DNS Read, and DNS Edit permissions.</li>
        <li>Validate the token, then open Domains to confirm matching zones are linked.</li>
        <li>Use View to compare records and run Import, Export, or Sync All as needed.</li>
      </ol>
    </section>

    <section class="section" id="troubleshooting">
      <p class="kicker">Troubleshooting</p>
      <h2>Useful paths and logs.</h2>
      <div class="grid two">
        <div class="card">
          <h3>Panel logs</h3>
          <pre><code>tail -n 200 /var/log/plesk/panel.log
tail -n 200 /var/log/sw-cp-server/error_log
tail -n 200 /usr/local/psa/admin/logs/panel.log</code></pre>
        </div>
        <div class="card">
          <h3>Important data</h3>
          <ul>
            <li>Module data: <code>/usr/local/psa/var/modules/cloudflare-pro</code></li>
            <li>SQLite storage: <code>cloudflare-pro.sqlite</code></li>
            <li>Frontend assets: <code>htdocs/public/assets</code></li>
            <li>Extension metadata: <code>meta.xml</code></li>
          </ul>
        </div>
      </div>
      <p class="note">API Logs are intended for explicit Cloudflare actions such as validate, sync, push, pull, delete, and proxy updates. Passive page refresh reads are excluded from API log writes.</p>
    </section>
  </main>

  <footer class="footer">
    <div class="footer-inner">
      <span>Cloudflare Pro for Plesk by Ghost Compiler</span>
      <span><a href="https://github.com/ghostcompiler/cloudflare-pro">GitHub</a> · <a href="https://github.com/ghostcompiler/cloudflare-pro/issues">Issues</a></span>
    </div>
  </footer>
  <script>
    (function () {
      var compare = document.getElementById('compare');
      var range = document.getElementById('themeRange');
      var light = document.getElementById('lightShot');
      var dark = document.getElementById('darkShot');
      var buttons = Array.prototype.slice.call(document.querySelectorAll('.thumbs button'));
      var copyButtons = Array.prototype.slice.call(document.querySelectorAll('pre code'));

      function setSplit(value) {
        compare.style.setProperty('--split', value + '%');
      }

      function setView(button) {
        var view = button.getAttribute('data-view');
        var label = button.getAttribute('data-label');
        light.src = 'screenshots/light_' + view + '.png';
        dark.src = 'screenshots/dark_' + view + '.png';
        light.alt = 'Cloudflare Pro ' + label + ' light theme';
        dark.alt = 'Cloudflare Pro ' + label + ' dark theme';
        buttons.forEach(function (item) {
          item.classList.toggle('active', item === button);
        });
      }

      if (!compare || !range || !light || !dark) {
        return;
      }

      range.addEventListener('input', function () {
        setSplit(range.value);
      });

      buttons.forEach(function (button) {
        button.addEventListener('click', function () {
          setView(button);
        });
      });

      copyButtons.forEach(function (code) {
        var pre = code.parentElement;
        if (!pre) {
          return;
        }

        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'copy-code';
        button.textContent = 'Copy';
        button.setAttribute('aria-label', 'Copy command');
        button.addEventListener('click', function () {
          var text = code.textContent || '';
          navigator.clipboard.writeText(text).then(function () {
            button.textContent = 'Copied';
            window.setTimeout(function () {
              button.textContent = 'Copy';
            }, 1400);
          }, function () {
            button.textContent = 'Select';
            window.setTimeout(function () {
              button.textContent = 'Copy';
            }, 1400);
          });
        });
        pre.appendChild(button);
      });

      setSplit(range.value);
    }());
  </script>
</body>
</html>