Changeset 3777263
- Timestamp:
- 10/31/2026 06:18:34 PM (less than one hour ago)
- Location:
- cachenova
- Files:
-
- 26 added
- 13 edited
-
tags/1.1.0 (added)
-
tags/1.1.0/LICENSE.txt (added)
-
tags/1.1.0/assets (added)
-
tags/1.1.0/assets/css (added)
-
tags/1.1.0/assets/css/cn-admin.css (added)
-
tags/1.1.0/assets/css/cn-admin.min.css (added)
-
tags/1.1.0/assets/js (added)
-
tags/1.1.0/assets/js/cn-admin.js (added)
-
tags/1.1.0/assets/js/cn-admin.min.js (added)
-
tags/1.1.0/cachenova.php (added)
-
tags/1.1.0/includes (added)
-
tags/1.1.0/includes/admin (added)
-
tags/1.1.0/includes/admin/admin.php (added)
-
tags/1.1.0/includes/core (added)
-
tags/1.1.0/includes/core/class-cache-drop-in-manager.php (added)
-
tags/1.1.0/integration (added)
-
tags/1.1.0/integration/class-rlcc-divi-integration.php (added)
-
tags/1.1.0/languages (added)
-
tags/1.1.0/languages/cachenova-strings.xml (added)
-
tags/1.1.0/languages/cachenova.pot (added)
-
tags/1.1.0/readme.txt (added)
-
tags/1.1.0/templates (added)
-
tags/1.1.0/templates/advanced-cache-template.php (added)
-
tags/1.1.0/templates/object-cache-template.php (added)
-
tags/1.1.0/uninstall.php (added)
-
trunk/assets/css/cn-admin.css (modified) (1 diff)
-
trunk/assets/css/cn-admin.min.css (modified) (1 diff)
-
trunk/assets/js/cn-admin.js (modified) (24 diffs)
-
trunk/assets/js/cn-admin.min.js (modified) (1 diff)
-
trunk/cachenova.php (modified) (23 diffs)
-
trunk/includes/admin/admin.php (modified) (11 diffs)
-
trunk/includes/core/class-cache-drop-in-manager.php (modified) (3 diffs)
-
trunk/integration/class-rlcc-divi-integration.php (modified) (1 diff)
-
trunk/languages/cachenova-strings.xml (added)
-
trunk/languages/cachenova.pot (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/advanced-cache-template.php (modified) (1 diff)
-
trunk/templates/object-cache-template.php (modified) (1 diff)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
cachenova/trunk/assets/css/cn-admin.css
r3737033 r3777263 613 613 } 614 614 } 615 616 /* CacheNova 1.1.0 dashboard polish: scoped to the plugin settings screen */ 617 #rlcc-settings-root {--cn-accent:#6254e8;--cn-border:#e6e9f2;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;} 618 #rlcc-settings-root .rlcc-card {border:1px solid var(--cn-border);border-radius:18px;box-shadow:0 8px 28px rgba(29,34,71,.055);transition:box-shadow .2s ease;} 619 #rlcc-settings-root .rlcc-card:hover {box-shadow:0 12px 32px rgba(29,34,71,.09);} 620 #rlcc-settings-root button,#rlcc-settings-root select {transition:background-color .16s ease,border-color .16s ease,box-shadow .16s ease;} 621 #rlcc-settings-root button:focus-visible,#rlcc-settings-root select:focus-visible {outline:3px solid rgba(98,84,232,.38);outline-offset:2px;} 622 #rlcc-settings-root .rlcc-toast {border-radius:12px;box-shadow:0 12px 28px rgba(20,25,60,.14);} 623 @media (prefers-reduced-motion:reduce){#rlcc-settings-root *{transition:none!important;animation:none!important;}} 624 625 /* CacheNova 1.1.0 — dashboard design system (scoped to the plugin admin page) */ 626 body.settings_page_rlcc-settings {background:#f3f5fa;} 627 body.settings_page_rlcc-settings #wpcontent {background:#f3f5fa;} 628 .rlcc-wrap {margin:22px 24px 40px 4px;} 629 .rlcc-dashboard {--rlcc-accent:#635bdf;--rlcc-accent-light:#8679fa;--rlcc-accent-bg:#f0efff;--rlcc-border:#e7eaf2;--rlcc-text:#18223b;--rlcc-text-muted:#66728b;--rlcc-surface-soft:#f8f9fc;--rlcc-shadow:0 5px 25px rgba(22,35,73,.045);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:1540px;} 630 .rlcc-page-intro {display:flex;align-items:center;justify-content:space-between;gap:16px;margin:4px 0 22px;} 631 .rlcc-page-intro h1 {font-size:30px;line-height:1.2;font-weight:760;letter-spacing:-.8px;color:#15203b;margin:5px 0 8px;} 632 .rlcc-page-intro p {margin:0;color:#6c7790;font-size:13px;} 633 .rlcc-eyebrow {color:#7466d8;font-weight:750;letter-spacing:1.5px;font-size:10px;} 634 .rlcc-intro-chip {background:#fff;border:1px solid #e7eaf2;border-radius:50px;color:#485674;padding:10px 16px;white-space:nowrap;font-size:12px;font-weight:650;box-shadow:0 2px 10px rgba(15,23,42,.025);} 635 .rlcc-hero {position:relative;overflow:hidden;background:linear-gradient(116deg,#242f72 0%,#5144bb 54%,#7865eb 100%);border:1px solid rgba(255,255,255,.15);border-radius:20px;padding:29px 34px;box-shadow:0 16px 38px rgba(67,56,163,.16);margin-bottom:20px;} 636 .rlcc-hero::after {content:"";position:absolute;width:340px;height:340px;right:-90px;top:-230px;border:60px solid rgba(255,255,255,.055);border-radius:50%;pointer-events:none;} 637 .rlcc-hero-title {display:flex;align-items:center;gap:10px;font-size:26px;letter-spacing:-.6px;color:#fff;} 638 .rlcc-brand-mark {display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:11px;background:rgba(255,255,255,.19);font-size:24px;color:#fff;} 639 .rlcc-version {font-size:10px;font-weight:650;letter-spacing:.2px;padding:5px 8px;border:1px solid rgba(255,255,255,.24);border-radius:6px;color:#e8e7ff;} 640 .rlcc-status-pill {background:rgba(35,207,148,.15);border:1px solid rgba(132,255,205,.28);color:#d9fff0;font-size:11px;} 641 .rlcc-status-dot {background:#62f3b1;} 642 .rlcc-hero-meta {border-top:1px solid rgba(255,255,255,.17);padding-top:20px;margin-top:24px;} 643 .rlcc-countdown {font-variant-numeric:tabular-nums;background:rgba(255,255,255,.18);border-radius:8px;padding:7px 12px;} 644 .rlcc-card-grid {gap:18px;} 645 .rlcc-card {background:#fff;border:1px solid #e8ebf3;border-radius:17px;box-shadow:var(--rlcc-shadow);padding:24px 26px;transition:border-color .18s ease,box-shadow .18s ease;} 646 .rlcc-card:hover {border-color:#d7d9ed;box-shadow:0 9px 28px rgba(27,34,76,.075);} 647 .rlcc-card-title {display:flex;align-items:center;gap:10px;font-size:14px;letter-spacing:-.15px;color:#1d2942;margin-bottom:22px;} 648 .rlcc-card-title::before {display:none;} 649 .rlcc-card-symbol {display:inline-flex;align-items:center;justify-content:center;background:#f0efff;color:#6558d7;width:32px;height:32px;border-radius:10px;font-size:18px;font-weight:600;flex:none;} 650 .rlcc-field label {font-weight:650;color:#56617b;letter-spacing:.15px;} 651 .rlcc-select,.rlcc-dashboard input[type=text],.rlcc-dashboard input[type=number] {border:1px solid #dfe3ed;border-radius:9px;min-height:43px;box-shadow:none;} 652 .rlcc-dashboard select:focus,.rlcc-dashboard input:focus,.rlcc-dashboard button:focus-visible {outline:3px solid rgba(99,91,223,.22);outline-offset:2px;border-color:#7768eb;} 653 .rlcc-switch {transition:background .15s ease,border-color .15s ease,transform .15s ease;} 654 .rlcc-switch:active {transform:scale(.97);} 655 .rlcc-switch--on {background:#e9f8f0;border-color:#a6e6c4;color:#147a46;} 656 .rlcc-toast-container {z-index:999999;max-width:min(400px,calc(100vw - 32px));} 657 .rlcc-toast {border-radius:12px;box-shadow:0 12px 35px rgba(20,27,55,.16);border:1px solid rgba(0,0,0,.04);} 658 @media(max-width:782px){.rlcc-wrap{margin:12px 12px 28px 0}.rlcc-page-intro{align-items:flex-start}.rlcc-page-intro h1{font-size:24px}.rlcc-intro-chip{display:none}.rlcc-hero{padding:22px}.rlcc-card{padding:19px}.rlcc-hero-title{font-size:23px}} 659 @media(prefers-reduced-motion:reduce){.rlcc-card,.rlcc-switch{transition:none}} 660 661 .rlcc-divi-compat{margin:18px 0;padding:22px;border-left:3px solid #7563f5}.rlcc-divi-compat p{color:#6b7280;line-height:1.55} 662 663 /* CacheNova Divi compatibility control */ 664 .rlcc-divi-compat{margin:20px 0;padding:24px!important;border:1px solid #dce2f2!important;border-left:4px solid #6955ee!important}.rlcc-divi-compat p{color:#526079;margin:12px 0 18px;line-height:1.6} 665 666 /* CacheNova tabs, database schedule row, footer */ 667 .rlcc-tabs{display:flex;gap:8px;margin:0 0 20px;padding:6px;background:#fff;border:1px solid #e8ebf3;border-radius:14px;box-shadow:var(--rlcc-shadow);width:fit-content;max-width:100%;flex-wrap:wrap} 668 .rlcc-tab{appearance:none;border:0;background:transparent;color:#56617b;font:inherit;font-size:13.5px;font-weight:650;padding:10px 18px;border-radius:10px;cursor:pointer;transition:background .15s ease,color .15s ease} 669 .rlcc-tab:hover{background:#f4f3ff;color:#4a3fc7} 670 .rlcc-tab--active,.rlcc-tab--active:hover{background:#635bdf;color:#fff} 671 .rlcc-db-status{display:flex;align-items:center;min-height:43px;padding:9px 14px;border-radius:9px;font-size:13.5px;font-weight:600;border:1px solid #dfe3ed;background:#f8f9fc;color:#56617b} 672 .rlcc-db-status--on{background:#e9f8f0;border-color:#a6e6c4;color:#147a46} 673 .rlcc-footer-grid{margin-top:20px} 674 @media(max-width:782px){.rlcc-tabs{width:100%}.rlcc-tab{flex:1;text-align:center;padding:10px 12px}} 675 676 /* CacheNova confirmation modal */ 677 .rlcc-modal-overlay{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(18,22,44,.55);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);animation:rlccFade .15s ease} 678 .rlcc-modal{width:100%;max-width:440px;background:#fff;border-radius:20px;padding:30px 30px 24px;text-align:center;box-shadow:0 24px 70px rgba(18,22,44,.35);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;animation:rlccPop .18s cubic-bezier(.2,.9,.3,1.2)} 679 .rlcc-modal-icon{width:56px;height:56px;margin:0 auto 16px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:800;line-height:1} 680 .rlcc-modal--info .rlcc-modal-icon{background:#f0efff;color:#635bdf} 681 .rlcc-modal--warning .rlcc-modal-icon{background:#fff4e0;color:#d98200} 682 .rlcc-modal--danger .rlcc-modal-icon{background:#fdeceb;color:#c0392b} 683 .rlcc-modal-title{margin:0 0 10px;font-size:19px;font-weight:700;color:#18223b;letter-spacing:-.2px} 684 .rlcc-modal-text{margin:0;font-size:14px;line-height:1.6;color:#56617b} 685 .rlcc-modal-points{list-style:none;margin:16px 0 0;padding:12px 16px;background:#f8f9fc;border:1px solid #e7eaf2;border-radius:12px;text-align:left} 686 .rlcc-modal-points li{position:relative;padding:4px 0 4px 20px;font-size:13px;color:#3b4560} 687 .rlcc-modal-points li::before{content:"";position:absolute;left:3px;top:11px;width:6px;height:6px;border-radius:50%;background:#9aa3bb} 688 .rlcc-modal-actions{display:flex;gap:10px;margin-top:24px} 689 .rlcc-modal-btn{flex:1;appearance:none;border:1px solid transparent;border-radius:11px;min-height:44px;padding:0 18px;font:inherit;font-size:14px;font-weight:650;cursor:pointer;transition:background .15s ease,transform .1s ease,box-shadow .15s ease} 690 .rlcc-modal-btn:active{transform:scale(.98)} 691 .rlcc-modal-btn:focus-visible{outline:3px solid rgba(99,91,223,.3);outline-offset:2px} 692 .rlcc-modal-btn--ghost{background:#fff;border-color:#dfe3ed;color:#3b4560} 693 .rlcc-modal-btn--ghost:hover{background:#f4f5fa} 694 .rlcc-modal-btn--info{background:#635bdf;color:#fff} 695 .rlcc-modal-btn--info:hover{background:#5249cc} 696 .rlcc-modal-btn--warning{background:#d98200;color:#fff} 697 .rlcc-modal-btn--warning:hover{background:#bf7200} 698 .rlcc-modal-btn--danger{background:#c0392b;color:#fff} 699 .rlcc-modal-btn--danger:hover{background:#a63023} 700 @keyframes rlccFade{from{opacity:0}to{opacity:1}} 701 @keyframes rlccPop{from{opacity:0;transform:translateY(8px) scale(.96)}to{opacity:1;transform:none}} 702 @media(max-width:480px){.rlcc-modal{padding:24px 20px 20px}.rlcc-modal-actions{flex-direction:column-reverse}} 703 @media(prefers-reduced-motion:reduce){.rlcc-modal-overlay,.rlcc-modal{animation:none}} 704 705 /* Database Optimization tab: one row, three columns */ 706 .rlcc-card-grid--three{grid-template-columns:repeat(3,minmax(0,1fr));align-items:stretch} 707 .rlcc-card-grid--three .rlcc-card{display:flex;flex-direction:column} 708 .rlcc-card-grid--three .rlcc-grid{grid-template-columns:1fr} 709 .rlcc-card-grid--three .rlcc-db-grid{grid-template-columns:1fr;margin:0 0 18px} 710 .rlcc-card-grid--three .rlcc-clear-btn{align-self:flex-start} 711 .rlcc-card-grid--three .rlcc-hint{margin:14px 0 0} 712 .rlcc-card-grid--three .rlcc-danger-row{flex-direction:column;align-items:flex-start;gap:14px} 713 @media(max-width:1100px){.rlcc-card-grid--three{grid-template-columns:1fr}} -
cachenova/trunk/assets/css/cn-admin.min.css
r3737033 r3777263 1 :root{--rlcc-accent:#6b5ce7;--rlcc-accent-light:#8b7cf0;--rlcc-accent-bg:#f0eeff;--rlcc-ok:#1a7f37;--rlcc-ok-bg:#eaf7ec;--rlcc-danger:#c0392b;--rlcc-danger-bg:#fdf3f2;--rlcc-warn:#b5820a;--rlcc-warn-bg:#fbf3e0;--rlcc-text:#1e1e1e;--rlcc-text-muted:#6b7280;--rlcc-border:#e4e6eb;--rlcc-surface:#ffffff;--rlcc-surface-soft:#f7f7fb;--rlcc-radius-lg:16px;--rlcc-radius-md:10px;--rlcc-radius-sm:7px;--rlcc-shadow:0 1px 2px rgba(20,20,43,0.04),0 8px 24px rgba(20,20,43,0.06);--rlcc-shadow-hover:0 4px 10px rgba(20,20,43,0.06),0 14px 32px rgba(20,20,43,0.10)}.rlcc-wrap{max-width:none}.rlcc-dashboard{box-sizing:border-box;width:100%;max-width:1600px;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--rlcc-text)}.rlcc-dashboard *,.rlcc-dashboard *::before,.rlcc-dashboard *::after{box-sizing:border-box}.rlcc-dashboard-loading{color:var(--rlcc-text-muted);text-align:center;padding:80px 0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}.rlcc-hero{background:linear-gradient(120deg,#5b4bd6 0%,#6b5ce7 45%,#8b7cf0 100%);border-radius:var(--rlcc-radius-lg);padding:28px 32px;color:#fff;box-shadow:0 10px 30px rgba(107,92,231,0.28);margin-bottom:22px}.rlcc-hero-top{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap}.rlcc-hero-title-block{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.rlcc-hero-title{margin:0;font-size:22px;font-weight:700;letter-spacing:-0.01em}.rlcc-status-pill{display:inline-flex;align-items:center;gap:8px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;background:rgba(255,255,255,0.16);padding:6px 14px;border-radius:999px}.rlcc-status-dot{width:8px;height:8px;border-radius:50%;background:#6dffb0;box-shadow:0 0 0 3px rgba(109,255,176,0.30)}.rlcc-hero-meta{display:flex;align-items:center;gap:24px;flex-wrap:wrap;margin-top:20px;padding-top:18px;border-top:1px solid rgba(255,255,255,0.18);font-size:13px}.rlcc-hero-meta-item{color:rgba(255,255,255,0.85)}.rlcc-hero-meta-item strong{color:#fff}.rlcc-hero-meta-item--divi{font-weight:600}.rlcc-countdown-row{display:flex;align-items:center;gap:10px}.rlcc-countdown{font-variant-numeric:tabular-nums;font-size:16px;font-weight:700;color:#fff;background:rgba(255,255,255,0.18);padding:3px 12px;border-radius:6px;letter-spacing:0.02em}.rlcc-clear-btn{padding:12px 22px;border-radius:999px;border:none;background:#fff;color:var(--rlcc-accent);font-size:14px;font-weight:700;cursor:pointer;transition:transform 0.1s ease,box-shadow 0.15s ease;box-shadow:0 2px 10px rgba(0,0,0,0.15);white-space:nowrap}.rlcc-clear-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,0.22)}.rlcc-clear-btn:disabled{opacity:0.65;cursor:not-allowed}.rlcc-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;width:100%;align-items:start}.rlcc-card{background:var(--rlcc-surface);border:1px solid var(--rlcc-border);border-radius:var(--rlcc-radius-lg);padding:24px 26px;box-shadow:var(--rlcc-shadow);transition:box-shadow 0.15s ease}.rlcc-card:hover{box-shadow:var(--rlcc-shadow-hover)}.rlcc-card--wide{grid-column:1 / -1}.rlcc-card--danger{border-color:#f3d4d0}.rlcc-card-title{margin:0 0 18px;font-size:14px;font-weight:700;color:var(--rlcc-text);text-transform:uppercase;letter-spacing:0.04em;padding-left:12px;border-left:3px solid var(--rlcc-accent)}.rlcc-card-title--danger{border-left-color:var(--rlcc-danger);color:var(--rlcc-danger)}.rlcc-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.rlcc-grid--4{grid-template-columns:repeat(4,1fr)}@media (max-width:560px){.rlcc-grid--4{grid-template-columns:repeat(2,1fr)}}.rlcc-field label{display:block;font-size:11.5px;font-weight:600;color:var(--rlcc-text-muted);margin-bottom:7px;text-transform:uppercase;letter-spacing:0.03em}.rlcc-select,.rlcc-input{width:100%;padding:9px 10px;border-radius:var(--rlcc-radius-sm);border:1px solid var(--rlcc-border);background:var(--rlcc-surface-soft);font-size:14px;color:var(--rlcc-text)}.rlcc-select:focus,.rlcc-input:focus{outline:none;border-color:var(--rlcc-accent);box-shadow:0 0 0 3px rgba(107,92,231,0.15)}.rlcc-switch{display:flex;align-items:center;gap:8px;padding:8px 14px;border-radius:999px;border:1px solid var(--rlcc-border);background:var(--rlcc-surface-soft);cursor:pointer;font-size:13px;font-weight:600;color:var(--rlcc-text)}.rlcc-switch:disabled{opacity:0.55;cursor:not-allowed}.rlcc-switch--on{background:var(--rlcc-ok-bg);border-color:var(--rlcc-ok);color:var(--rlcc-ok)}.rlcc-switch-knob{width:14px;height:14px;border-radius:50%;background:#bbb;transition:background 0.15s ease;flex-shrink:0}.rlcc-switch--on .rlcc-switch-knob{background:var(--rlcc-ok)}.rlcc-hint{margin:14px 0 0;font-size:12px;line-height:1.5;color:var(--rlcc-text-muted)}.rlcc-infotip{position:relative;display:inline-flex;align-items:center;justify-content:center;margin-left:6px;cursor:help;vertical-align:middle}.rlcc-infotip-icon{width:16px;height:16px;border-radius:50%;background:#e9e6fc;color:var(--rlcc-brand,#6b5ce7);font-size:11px;font-weight:700;font-style:italic;font-family:Georgia,'Times New Roman',serif;display:flex;align-items:center;justify-content:center;line-height:1}.rlcc-infotip-text{visibility:hidden;opacity:0;position:absolute;bottom:130%;left:50%;transform:translateX(-50%);background:#1e1e1e;color:#fff;padding:8px 12px;border-radius:6px;font-size:12px;font-weight:400;line-height:1.45;width:260px;text-align:left;transition:opacity 0.12s ease;z-index:30;pointer-events:none;box-shadow:0 4px 14px rgba(0,0,0,0.25)}.rlcc-infotip-text::after{content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:#1e1e1e}.rlcc-infotip:hover .rlcc-infotip-text,.rlcc-infotip:focus .rlcc-infotip-text,.rlcc-infotip:focus-within .rlcc-infotip-text{visibility:visible;opacity:1}.rlcc-db-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin:16px 0 20px}.rlcc-db-checkbox{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;background:var(--rlcc-bg-subtle,#f7f7f8);font-size:13px;cursor:pointer}.rlcc-db-checkbox input[type="checkbox"]{width:16px;height:16px;flex-shrink:0}.rlcc-db-checkbox span:nth-child(2){flex:1}.rlcc-db-count{font-size:11px;font-weight:700;color:var(--rlcc-brand,#6b5ce7);background:#e9e6fc;padding:2px 8px;border-radius:999px;min-width:20px;text-align:center}.rlcc-danger-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 16px;background:var(--rlcc-danger-bg);border:1px solid #f3d4d0;border-radius:var(--rlcc-radius-md);flex-wrap:wrap}.rlcc-danger-text label{display:block;font-size:13px;font-weight:700;color:#922b21;margin-bottom:4px}.rlcc-danger-text .rlcc-hint{margin:0;color:#a04b42;max-width:480px}.rlcc-switch--danger.rlcc-switch--off{background:var(--rlcc-ok-bg);border-color:var(--rlcc-ok);color:var(--rlcc-ok)}.rlcc-switch--danger.rlcc-switch--off .rlcc-switch-knob{background:var(--rlcc-ok)}.rlcc-switch--danger.rlcc-switch--on{background:#fceceb;border-color:var(--rlcc-danger);color:var(--rlcc-danger)}.rlcc-switch--danger.rlcc-switch--on .rlcc-switch-knob{background:var(--rlcc-danger)}.rlcc-oc-backend-row{display:flex;gap:12px;margin-top:14px;flex-wrap:wrap}.rlcc-oc-backend-row .rlcc-field{flex:1;min-width:140px}.rlcc-oc-status{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:4px 10px;border-radius:999px;margin-top:12px}.rlcc-oc-status--ok{background:var(--rlcc-ok-bg);color:var(--rlcc-ok)}.rlcc-oc-status--error{background:var(--rlcc-danger-bg);color:var(--rlcc-danger)}.rlcc-oc-status--idle{background:var(--rlcc-accent-bg);color:var(--rlcc-accent)}.rlcc-oc-test-btn{margin-top:12px;padding:8px 16px;border-radius:var(--rlcc-radius-sm);border:1px solid var(--rlcc-border);background:#fff;font-size:13px;font-weight:600;cursor:pointer}.rlcc-oc-test-btn:disabled{opacity:0.6;cursor:not-allowed}.rlcc-oc-unavailable{font-size:11.5px;color:#b23b3b;margin:0 0 12px;line-height:1.5}.rlcc-report-list{list-style:none;margin:12px 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px}.rlcc-report-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--rlcc-radius-sm);font-size:13px;font-weight:500;background:var(--rlcc-surface-soft)}.rlcc-report-icon{font-weight:700;width:18px;text-align:center;flex-shrink:0}.rlcc-report-label{flex:1;color:var(--rlcc-text)}.rlcc-report-badge{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:0.03em;padding:2px 8px;border-radius:999px;white-space:nowrap}.rlcc-report-ok{background:var(--rlcc-ok-bg)}.rlcc-report-ok .rlcc-report-icon{color:var(--rlcc-ok)}.rlcc-report-ok .rlcc-report-badge{background:var(--rlcc-ok);color:#fff}.rlcc-report-triggered{background:var(--rlcc-accent-bg)}.rlcc-report-triggered .rlcc-report-icon{color:var(--rlcc-accent)}.rlcc-report-triggered .rlcc-report-badge{background:var(--rlcc-accent);color:#fff}.rlcc-report-failed{background:var(--rlcc-danger-bg)}.rlcc-report-failed .rlcc-report-icon{color:var(--rlcc-danger)}.rlcc-report-failed .rlcc-report-badge{background:var(--rlcc-danger);color:#fff}.rlcc-toast-container{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:10px;z-index:100000}.rlcc-toast{display:flex;align-items:center;gap:10px;padding:12px 18px;border-radius:10px;background:#1e1e1e;color:#fff;font-size:13px;font-weight:500;box-shadow:0 6px 20px rgba(0,0,0,0.25);cursor:pointer;animation:rlcc-toast-in 0.25s ease;min-width:220px}.rlcc-toast--success{background:var(--rlcc-ok)}.rlcc-toast--error{background:#c53030}.rlcc-toast--info{background:#1e1e1e}.rlcc-toast-icon{font-weight:700}@keyframes rlcc-toast-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@media (max-width:720px){.rlcc-hero{padding:22px 20px}.rlcc-hero-top{flex-direction:column;align-items:flex-start}.rlcc-clear-btn{width:100%}.rlcc-hero-meta{flex-direction:column;align-items:flex-start;gap:10px}.rlcc-card{padding:20px}.rlcc-grid{grid-template-columns:1fr}.rlcc-card-grid{grid-template-columns:1fr}} 1 :root { 2 --rlcc-accent: #6b5ce7; 3 --rlcc-accent-light: #8b7cf0; 4 --rlcc-accent-bg: #f0eeff; 5 --rlcc-ok: #1a7f37; 6 --rlcc-ok-bg: #eaf7ec; 7 --rlcc-danger: #c0392b; 8 --rlcc-danger-bg: #fdf3f2; 9 --rlcc-warn: #b5820a; 10 --rlcc-warn-bg: #fbf3e0; 11 --rlcc-text: #1e1e1e; 12 --rlcc-text-muted: #6b7280; 13 --rlcc-border: #e4e6eb; 14 --rlcc-surface: #ffffff; 15 --rlcc-surface-soft: #f7f7fb; 16 --rlcc-radius-lg: 16px; 17 --rlcc-radius-md: 10px; 18 --rlcc-radius-sm: 7px; 19 --rlcc-shadow: 0 1px 2px rgba(20,20,43,0.04), 0 8px 24px rgba(20,20,43,0.06); 20 --rlcc-shadow-hover: 0 4px 10px rgba(20,20,43,0.06), 0 14px 32px rgba(20,20,43,0.10); 21 } 22 23 .rlcc-wrap { 24 max-width: none; 25 } 26 27 .rlcc-dashboard { 28 box-sizing: border-box; 29 width: 100%; 30 max-width: 1600px; 31 margin: 0; 32 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 33 color: var(--rlcc-text); 34 } 35 36 .rlcc-dashboard *, 37 .rlcc-dashboard *::before, 38 .rlcc-dashboard *::after { 39 box-sizing: border-box; 40 } 41 42 .rlcc-dashboard-loading { 43 color: var(--rlcc-text-muted); 44 text-align: center; 45 padding: 80px 0; 46 font-size: 14px; 47 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 48 } 49 50 .rlcc-hero { 51 background: linear-gradient(120deg, #5b4bd6 0%, #6b5ce7 45%, #8b7cf0 100%); 52 border-radius: var(--rlcc-radius-lg); 53 padding: 28px 32px; 54 color: #fff; 55 box-shadow: 0 10px 30px rgba(107,92,231,0.28); 56 margin-bottom: 22px; 57 } 58 59 .rlcc-hero-top { 60 display: flex; 61 align-items: center; 62 justify-content: space-between; 63 gap: 20px; 64 flex-wrap: wrap; 65 } 66 67 .rlcc-hero-title-block { 68 display: flex; 69 align-items: center; 70 gap: 16px; 71 flex-wrap: wrap; 72 } 73 74 .rlcc-hero-title { 75 margin: 0; 76 font-size: 22px; 77 font-weight: 700; 78 letter-spacing: -0.01em; 79 } 80 81 .rlcc-status-pill { 82 display: inline-flex; 83 align-items: center; 84 gap: 8px; 85 font-size: 12px; 86 font-weight: 600; 87 text-transform: uppercase; 88 letter-spacing: 0.04em; 89 background: rgba(255,255,255,0.16); 90 padding: 6px 14px; 91 border-radius: 999px; 92 } 93 94 .rlcc-status-dot { 95 width: 8px; 96 height: 8px; 97 border-radius: 50%; 98 background: #6dffb0; 99 box-shadow: 0 0 0 3px rgba(109,255,176,0.30); 100 } 101 102 .rlcc-hero-meta { 103 display: flex; 104 align-items: center; 105 gap: 24px; 106 flex-wrap: wrap; 107 margin-top: 20px; 108 padding-top: 18px; 109 border-top: 1px solid rgba(255,255,255,0.18); 110 font-size: 13px; 111 } 112 113 .rlcc-hero-meta-item { 114 color: rgba(255,255,255,0.85); 115 } 116 117 .rlcc-hero-meta-item strong { 118 color: #fff; 119 } 120 121 .rlcc-hero-meta-item--divi { 122 font-weight: 600; 123 } 124 125 .rlcc-countdown-row { 126 display: flex; 127 align-items: center; 128 gap: 10px; 129 } 130 131 .rlcc-countdown { 132 font-variant-numeric: tabular-nums; 133 font-size: 16px; 134 font-weight: 700; 135 color: #fff; 136 background: rgba(255,255,255,0.18); 137 padding: 3px 12px; 138 border-radius: 6px; 139 letter-spacing: 0.02em; 140 } 141 142 .rlcc-clear-btn { 143 padding: 12px 22px; 144 border-radius: 999px; 145 border: none; 146 background: #fff; 147 color: var(--rlcc-accent); 148 font-size: 14px; 149 font-weight: 700; 150 cursor: pointer; 151 transition: transform 0.1s ease, box-shadow 0.15s ease; 152 box-shadow: 0 2px 10px rgba(0,0,0,0.15); 153 white-space: nowrap; 154 } 155 156 .rlcc-clear-btn:hover:not(:disabled) { 157 transform: translateY(-1px); 158 box-shadow: 0 6px 16px rgba(0,0,0,0.22); 159 } 160 161 .rlcc-clear-btn:disabled { 162 opacity: 0.65; 163 cursor: not-allowed; 164 } 165 166 .rlcc-card-grid { 167 display: grid; 168 grid-template-columns: repeat(2, minmax(0, 1fr)); 169 gap: 20px; 170 width: 100%; 171 align-items: start; 172 } 173 174 .rlcc-card { 175 background: var(--rlcc-surface); 176 border: 1px solid var(--rlcc-border); 177 border-radius: var(--rlcc-radius-lg); 178 padding: 24px 26px; 179 box-shadow: var(--rlcc-shadow); 180 transition: box-shadow 0.15s ease; 181 } 182 183 .rlcc-card:hover { 184 box-shadow: var(--rlcc-shadow-hover); 185 } 186 187 .rlcc-card--wide { 188 grid-column: 1 / -1; 189 } 190 191 .rlcc-card--danger { 192 border-color: #f3d4d0; 193 } 194 195 .rlcc-card-title { 196 margin: 0 0 18px; 197 font-size: 14px; 198 font-weight: 700; 199 color: var(--rlcc-text); 200 text-transform: uppercase; 201 letter-spacing: 0.04em; 202 padding-left: 12px; 203 border-left: 3px solid var(--rlcc-accent); 204 } 205 206 .rlcc-card-title--danger { 207 border-left-color: var(--rlcc-danger); 208 color: var(--rlcc-danger); 209 } 210 211 .rlcc-grid { 212 display: grid; 213 grid-template-columns: 1fr 1fr; 214 gap: 18px; 215 } 216 217 .rlcc-grid--4 { 218 grid-template-columns: repeat(4, 1fr); 219 } 220 221 @media (max-width: 560px) { 222 .rlcc-grid--4 { 223 grid-template-columns: repeat(2, 1fr); 224 } 225 } 226 227 .rlcc-field label { 228 display: block; 229 font-size: 11.5px; 230 font-weight: 600; 231 color: var(--rlcc-text-muted); 232 margin-bottom: 7px; 233 text-transform: uppercase; 234 letter-spacing: 0.03em; 235 } 236 237 .rlcc-select, 238 .rlcc-input { 239 width: 100%; 240 padding: 9px 10px; 241 border-radius: var(--rlcc-radius-sm); 242 border: 1px solid var(--rlcc-border); 243 background: var(--rlcc-surface-soft); 244 font-size: 14px; 245 color: var(--rlcc-text); 246 } 247 248 .rlcc-select:focus, 249 .rlcc-input:focus { 250 outline: none; 251 border-color: var(--rlcc-accent); 252 box-shadow: 0 0 0 3px rgba(107,92,231,0.15); 253 } 254 255 .rlcc-switch { 256 display: flex; 257 align-items: center; 258 gap: 8px; 259 padding: 8px 14px; 260 border-radius: 999px; 261 border: 1px solid var(--rlcc-border); 262 background: var(--rlcc-surface-soft); 263 cursor: pointer; 264 font-size: 13px; 265 font-weight: 600; 266 color: var(--rlcc-text); 267 } 268 269 .rlcc-switch:disabled { 270 opacity: 0.55; 271 cursor: not-allowed; 272 } 273 274 .rlcc-switch--on { 275 background: var(--rlcc-ok-bg); 276 border-color: var(--rlcc-ok); 277 color: var(--rlcc-ok); 278 } 279 280 .rlcc-switch-knob { 281 width: 14px; 282 height: 14px; 283 border-radius: 50%; 284 background: #bbb; 285 transition: background 0.15s ease; 286 flex-shrink: 0; 287 } 288 289 .rlcc-switch--on .rlcc-switch-knob { 290 background: var(--rlcc-ok); 291 } 292 293 .rlcc-hint { 294 margin: 14px 0 0; 295 font-size: 12px; 296 line-height: 1.5; 297 color: var(--rlcc-text-muted); 298 } 299 300 .rlcc-infotip { 301 position: relative; 302 display: inline-flex; 303 align-items: center; 304 justify-content: center; 305 margin-left: 6px; 306 cursor: help; 307 vertical-align: middle; 308 } 309 .rlcc-infotip-icon { 310 width: 16px; 311 height: 16px; 312 border-radius: 50%; 313 background: #e9e6fc; 314 color: var(--rlcc-brand, #6b5ce7); 315 font-size: 11px; 316 font-weight: 700; 317 font-style: italic; 318 font-family: Georgia, 'Times New Roman', serif; 319 display: flex; 320 align-items: center; 321 justify-content: center; 322 line-height: 1; 323 } 324 .rlcc-infotip-text { 325 visibility: hidden; 326 opacity: 0; 327 position: absolute; 328 bottom: 130%; 329 left: 50%; 330 transform: translateX(-50%); 331 background: #1e1e1e; 332 color: #fff; 333 padding: 8px 12px; 334 border-radius: 6px; 335 font-size: 12px; 336 font-weight: 400; 337 line-height: 1.45; 338 width: 260px; 339 text-align: left; 340 transition: opacity 0.12s ease; 341 z-index: 30; 342 pointer-events: none; 343 box-shadow: 0 4px 14px rgba(0,0,0,0.25); 344 } 345 .rlcc-infotip-text::after { 346 content: ''; 347 position: absolute; 348 top: 100%; 349 left: 50%; 350 transform: translateX(-50%); 351 border: 5px solid transparent; 352 border-top-color: #1e1e1e; 353 } 354 .rlcc-infotip:hover .rlcc-infotip-text, 355 .rlcc-infotip:focus .rlcc-infotip-text, 356 .rlcc-infotip:focus-within .rlcc-infotip-text { 357 visibility: visible; 358 opacity: 1; 359 } 360 361 .rlcc-db-grid { 362 display: grid; 363 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 364 gap: 10px; 365 margin: 16px 0 20px; 366 } 367 368 .rlcc-db-checkbox { 369 display: flex; 370 align-items: center; 371 gap: 8px; 372 padding: 10px 12px; 373 border-radius: 8px; 374 background: var(--rlcc-bg-subtle, #f7f7f8); 375 font-size: 13px; 376 cursor: pointer; 377 } 378 379 .rlcc-db-checkbox input[type="checkbox"] { 380 width: 16px; 381 height: 16px; 382 flex-shrink: 0; 383 } 384 385 .rlcc-db-checkbox span:nth-child(2) { 386 flex: 1; 387 } 388 389 .rlcc-db-count { 390 font-size: 11px; 391 font-weight: 700; 392 color: var(--rlcc-brand, #6b5ce7); 393 background: #e9e6fc; 394 padding: 2px 8px; 395 border-radius: 999px; 396 min-width: 20px; 397 text-align: center; 398 } 399 400 .rlcc-danger-row { 401 display: flex; 402 align-items: center; 403 justify-content: space-between; 404 gap: 16px; 405 padding: 14px 16px; 406 background: var(--rlcc-danger-bg); 407 border: 1px solid #f3d4d0; 408 border-radius: var(--rlcc-radius-md); 409 flex-wrap: wrap; 410 } 411 412 .rlcc-danger-text label { 413 display: block; 414 font-size: 13px; 415 font-weight: 700; 416 color: #922b21; 417 margin-bottom: 4px; 418 } 419 420 .rlcc-danger-text .rlcc-hint { 421 margin: 0; 422 color: #a04b42; 423 max-width: 480px; 424 } 425 426 .rlcc-switch--danger.rlcc-switch--off { 427 background: var(--rlcc-ok-bg); 428 border-color: var(--rlcc-ok); 429 color: var(--rlcc-ok); 430 } 431 432 .rlcc-switch--danger.rlcc-switch--off .rlcc-switch-knob { 433 background: var(--rlcc-ok); 434 } 435 436 .rlcc-switch--danger.rlcc-switch--on { 437 background: #fceceb; 438 border-color: var(--rlcc-danger); 439 color: var(--rlcc-danger); 440 } 441 442 .rlcc-switch--danger.rlcc-switch--on .rlcc-switch-knob { 443 background: var(--rlcc-danger); 444 } 445 446 .rlcc-oc-backend-row { 447 display: flex; 448 gap: 12px; 449 margin-top: 14px; 450 flex-wrap: wrap; 451 } 452 453 .rlcc-oc-backend-row .rlcc-field { 454 flex: 1; 455 min-width: 140px; 456 } 457 458 .rlcc-oc-status { 459 display: inline-flex; 460 align-items: center; 461 gap: 6px; 462 font-size: 12px; 463 font-weight: 600; 464 padding: 4px 10px; 465 border-radius: 999px; 466 margin-top: 12px; 467 } 468 469 .rlcc-oc-status--ok { background: var(--rlcc-ok-bg); color: var(--rlcc-ok); } 470 .rlcc-oc-status--error { background: var(--rlcc-danger-bg); color: var(--rlcc-danger); } 471 .rlcc-oc-status--idle { background: var(--rlcc-accent-bg); color: var(--rlcc-accent); } 472 473 .rlcc-oc-test-btn { 474 margin-top: 12px; 475 padding: 8px 16px; 476 border-radius: var(--rlcc-radius-sm); 477 border: 1px solid var(--rlcc-border); 478 background: #fff; 479 font-size: 13px; 480 font-weight: 600; 481 cursor: pointer; 482 } 483 484 .rlcc-oc-test-btn:disabled { 485 opacity: 0.6; 486 cursor: not-allowed; 487 } 488 489 .rlcc-oc-unavailable { 490 font-size: 11.5px; 491 color: #b23b3b; 492 margin: 0 0 12px; 493 line-height: 1.5; 494 } 495 496 .rlcc-report-list { 497 list-style: none; 498 margin: 12px 0 0; 499 padding: 0; 500 display: grid; 501 grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); 502 gap: 8px; 503 } 504 505 .rlcc-report-row { 506 display: flex; 507 align-items: center; 508 gap: 10px; 509 padding: 9px 12px; 510 border-radius: var(--rlcc-radius-sm); 511 font-size: 13px; 512 font-weight: 500; 513 background: var(--rlcc-surface-soft); 514 } 515 516 .rlcc-report-icon { 517 font-weight: 700; 518 width: 18px; 519 text-align: center; 520 flex-shrink: 0; 521 } 522 523 .rlcc-report-label { 524 flex: 1; 525 color: var(--rlcc-text); 526 } 527 528 .rlcc-report-badge { 529 font-size: 10.5px; 530 font-weight: 700; 531 text-transform: uppercase; 532 letter-spacing: 0.03em; 533 padding: 2px 8px; 534 border-radius: 999px; 535 white-space: nowrap; 536 } 537 538 .rlcc-report-ok { background: var(--rlcc-ok-bg); } 539 .rlcc-report-ok .rlcc-report-icon { color: var(--rlcc-ok); } 540 .rlcc-report-ok .rlcc-report-badge { background: var(--rlcc-ok); color: #fff; } 541 542 .rlcc-report-triggered { background: var(--rlcc-accent-bg); } 543 .rlcc-report-triggered .rlcc-report-icon { color: var(--rlcc-accent); } 544 .rlcc-report-triggered .rlcc-report-badge { background: var(--rlcc-accent); color: #fff; } 545 546 .rlcc-report-failed { background: var(--rlcc-danger-bg); } 547 .rlcc-report-failed .rlcc-report-icon { color: var(--rlcc-danger); } 548 .rlcc-report-failed .rlcc-report-badge { background: var(--rlcc-danger); color: #fff; } 549 550 .rlcc-toast-container { 551 position: fixed; 552 bottom: 24px; 553 right: 24px; 554 display: flex; 555 flex-direction: column; 556 gap: 10px; 557 z-index: 100000; 558 } 559 560 .rlcc-toast { 561 display: flex; 562 align-items: center; 563 gap: 10px; 564 padding: 12px 18px; 565 border-radius: 10px; 566 background: #1e1e1e; 567 color: #fff; 568 font-size: 13px; 569 font-weight: 500; 570 box-shadow: 0 6px 20px rgba(0,0,0,0.25); 571 cursor: pointer; 572 animation: rlcc-toast-in 0.25s ease; 573 min-width: 220px; 574 } 575 576 .rlcc-toast--success { background: var(--rlcc-ok); } 577 .rlcc-toast--error { background: #c53030; } 578 .rlcc-toast--info { background: #1e1e1e; } 579 580 .rlcc-toast-icon { 581 font-weight: 700; 582 } 583 584 @keyframes rlcc-toast-in { 585 from { opacity: 0; transform: translateY(10px); } 586 to { opacity: 1; transform: translateY(0); } 587 } 588 589 @media (max-width: 720px) { 590 .rlcc-hero { 591 padding: 22px 20px; 592 } 593 .rlcc-hero-top { 594 flex-direction: column; 595 align-items: flex-start; 596 } 597 .rlcc-clear-btn { 598 width: 100%; 599 } 600 .rlcc-hero-meta { 601 flex-direction: column; 602 align-items: flex-start; 603 gap: 10px; 604 } 605 .rlcc-card { 606 padding: 20px; 607 } 608 .rlcc-grid { 609 grid-template-columns: 1fr; 610 } 611 .rlcc-card-grid { 612 grid-template-columns: 1fr; 613 } 614 } 615 616 /* CacheNova 1.1.0 dashboard polish: scoped to the plugin settings screen */ 617 #rlcc-settings-root {--cn-accent:#6254e8;--cn-border:#e6e9f2;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;} 618 #rlcc-settings-root .rlcc-card {border:1px solid var(--cn-border);border-radius:18px;box-shadow:0 8px 28px rgba(29,34,71,.055);transition:box-shadow .2s ease;} 619 #rlcc-settings-root .rlcc-card:hover {box-shadow:0 12px 32px rgba(29,34,71,.09);} 620 #rlcc-settings-root button,#rlcc-settings-root select {transition:background-color .16s ease,border-color .16s ease,box-shadow .16s ease;} 621 #rlcc-settings-root button:focus-visible,#rlcc-settings-root select:focus-visible {outline:3px solid rgba(98,84,232,.38);outline-offset:2px;} 622 #rlcc-settings-root .rlcc-toast {border-radius:12px;box-shadow:0 12px 28px rgba(20,25,60,.14);} 623 @media (prefers-reduced-motion:reduce){#rlcc-settings-root *{transition:none!important;animation:none!important;}} 624 625 /* CacheNova 1.1.0 — dashboard design system (scoped to the plugin admin page) */ 626 body.settings_page_rlcc-settings {background:#f3f5fa;} 627 body.settings_page_rlcc-settings #wpcontent {background:#f3f5fa;} 628 .rlcc-wrap {margin:22px 24px 40px 4px;} 629 .rlcc-dashboard {--rlcc-accent:#635bdf;--rlcc-accent-light:#8679fa;--rlcc-accent-bg:#f0efff;--rlcc-border:#e7eaf2;--rlcc-text:#18223b;--rlcc-text-muted:#66728b;--rlcc-surface-soft:#f8f9fc;--rlcc-shadow:0 5px 25px rgba(22,35,73,.045);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:1540px;} 630 .rlcc-page-intro {display:flex;align-items:center;justify-content:space-between;gap:16px;margin:4px 0 22px;} 631 .rlcc-page-intro h1 {font-size:30px;line-height:1.2;font-weight:760;letter-spacing:-.8px;color:#15203b;margin:5px 0 8px;} 632 .rlcc-page-intro p {margin:0;color:#6c7790;font-size:13px;} 633 .rlcc-eyebrow {color:#7466d8;font-weight:750;letter-spacing:1.5px;font-size:10px;} 634 .rlcc-intro-chip {background:#fff;border:1px solid #e7eaf2;border-radius:50px;color:#485674;padding:10px 16px;white-space:nowrap;font-size:12px;font-weight:650;box-shadow:0 2px 10px rgba(15,23,42,.025);} 635 .rlcc-hero {position:relative;overflow:hidden;background:linear-gradient(116deg,#242f72 0%,#5144bb 54%,#7865eb 100%);border:1px solid rgba(255,255,255,.15);border-radius:20px;padding:29px 34px;box-shadow:0 16px 38px rgba(67,56,163,.16);margin-bottom:20px;} 636 .rlcc-hero::after {content:"";position:absolute;width:340px;height:340px;right:-90px;top:-230px;border:60px solid rgba(255,255,255,.055);border-radius:50%;pointer-events:none;} 637 .rlcc-hero-title {display:flex;align-items:center;gap:10px;font-size:26px;letter-spacing:-.6px;color:#fff;} 638 .rlcc-brand-mark {display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:11px;background:rgba(255,255,255,.19);font-size:24px;color:#fff;} 639 .rlcc-version {font-size:10px;font-weight:650;letter-spacing:.2px;padding:5px 8px;border:1px solid rgba(255,255,255,.24);border-radius:6px;color:#e8e7ff;} 640 .rlcc-status-pill {background:rgba(35,207,148,.15);border:1px solid rgba(132,255,205,.28);color:#d9fff0;font-size:11px;} 641 .rlcc-status-dot {background:#62f3b1;} 642 .rlcc-hero-meta {border-top:1px solid rgba(255,255,255,.17);padding-top:20px;margin-top:24px;} 643 .rlcc-countdown {font-variant-numeric:tabular-nums;background:rgba(255,255,255,.18);border-radius:8px;padding:7px 12px;} 644 .rlcc-card-grid {gap:18px;} 645 .rlcc-card {background:#fff;border:1px solid #e8ebf3;border-radius:17px;box-shadow:var(--rlcc-shadow);padding:24px 26px;transition:border-color .18s ease,box-shadow .18s ease;} 646 .rlcc-card:hover {border-color:#d7d9ed;box-shadow:0 9px 28px rgba(27,34,76,.075);} 647 .rlcc-card-title {display:flex;align-items:center;gap:10px;font-size:14px;letter-spacing:-.15px;color:#1d2942;margin-bottom:22px;} 648 .rlcc-card-title::before {display:none;} 649 .rlcc-card-symbol {display:inline-flex;align-items:center;justify-content:center;background:#f0efff;color:#6558d7;width:32px;height:32px;border-radius:10px;font-size:18px;font-weight:600;flex:none;} 650 .rlcc-field label {font-weight:650;color:#56617b;letter-spacing:.15px;} 651 .rlcc-select,.rlcc-dashboard input[type=text],.rlcc-dashboard input[type=number] {border:1px solid #dfe3ed;border-radius:9px;min-height:43px;box-shadow:none;} 652 .rlcc-dashboard select:focus,.rlcc-dashboard input:focus,.rlcc-dashboard button:focus-visible {outline:3px solid rgba(99,91,223,.22);outline-offset:2px;border-color:#7768eb;} 653 .rlcc-switch {transition:background .15s ease,border-color .15s ease,transform .15s ease;} 654 .rlcc-switch:active {transform:scale(.97);} 655 .rlcc-switch--on {background:#e9f8f0;border-color:#a6e6c4;color:#147a46;} 656 .rlcc-toast-container {z-index:999999;max-width:min(400px,calc(100vw - 32px));} 657 .rlcc-toast {border-radius:12px;box-shadow:0 12px 35px rgba(20,27,55,.16);border:1px solid rgba(0,0,0,.04);} 658 @media(max-width:782px){.rlcc-wrap{margin:12px 12px 28px 0}.rlcc-page-intro{align-items:flex-start}.rlcc-page-intro h1{font-size:24px}.rlcc-intro-chip{display:none}.rlcc-hero{padding:22px}.rlcc-card{padding:19px}.rlcc-hero-title{font-size:23px}} 659 @media(prefers-reduced-motion:reduce){.rlcc-card,.rlcc-switch{transition:none}} 660 661 .rlcc-divi-compat{margin:18px 0;padding:22px;border-left:3px solid #7563f5}.rlcc-divi-compat p{color:#6b7280;line-height:1.55} 662 663 /* CacheNova Divi compatibility control */ 664 .rlcc-divi-compat{margin:20px 0;padding:24px!important;border:1px solid #dce2f2!important;border-left:4px solid #6955ee!important}.rlcc-divi-compat p{color:#526079;margin:12px 0 18px;line-height:1.6} 665 666 /* CacheNova tabs, database schedule row, footer */ 667 .rlcc-tabs{display:flex;gap:8px;margin:0 0 20px;padding:6px;background:#fff;border:1px solid #e8ebf3;border-radius:14px;box-shadow:var(--rlcc-shadow);width:fit-content;max-width:100%;flex-wrap:wrap} 668 .rlcc-tab{appearance:none;border:0;background:transparent;color:#56617b;font:inherit;font-size:13.5px;font-weight:650;padding:10px 18px;border-radius:10px;cursor:pointer;transition:background .15s ease,color .15s ease} 669 .rlcc-tab:hover{background:#f4f3ff;color:#4a3fc7} 670 .rlcc-tab--active,.rlcc-tab--active:hover{background:#635bdf;color:#fff} 671 .rlcc-db-status{display:flex;align-items:center;min-height:43px;padding:9px 14px;border-radius:9px;font-size:13.5px;font-weight:600;border:1px solid #dfe3ed;background:#f8f9fc;color:#56617b} 672 .rlcc-db-status--on{background:#e9f8f0;border-color:#a6e6c4;color:#147a46} 673 .rlcc-footer-grid{margin-top:20px} 674 @media(max-width:782px){.rlcc-tabs{width:100%}.rlcc-tab{flex:1;text-align:center;padding:10px 12px}} 675 676 /* CacheNova confirmation modal */ 677 .rlcc-modal-overlay{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(18,22,44,.55);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);animation:rlccFade .15s ease} 678 .rlcc-modal{width:100%;max-width:440px;background:#fff;border-radius:20px;padding:30px 30px 24px;text-align:center;box-shadow:0 24px 70px rgba(18,22,44,.35);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;animation:rlccPop .18s cubic-bezier(.2,.9,.3,1.2)} 679 .rlcc-modal-icon{width:56px;height:56px;margin:0 auto 16px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:800;line-height:1} 680 .rlcc-modal--info .rlcc-modal-icon{background:#f0efff;color:#635bdf} 681 .rlcc-modal--warning .rlcc-modal-icon{background:#fff4e0;color:#d98200} 682 .rlcc-modal--danger .rlcc-modal-icon{background:#fdeceb;color:#c0392b} 683 .rlcc-modal-title{margin:0 0 10px;font-size:19px;font-weight:700;color:#18223b;letter-spacing:-.2px} 684 .rlcc-modal-text{margin:0;font-size:14px;line-height:1.6;color:#56617b} 685 .rlcc-modal-points{list-style:none;margin:16px 0 0;padding:12px 16px;background:#f8f9fc;border:1px solid #e7eaf2;border-radius:12px;text-align:left} 686 .rlcc-modal-points li{position:relative;padding:4px 0 4px 20px;font-size:13px;color:#3b4560} 687 .rlcc-modal-points li::before{content:"";position:absolute;left:3px;top:11px;width:6px;height:6px;border-radius:50%;background:#9aa3bb} 688 .rlcc-modal-actions{display:flex;gap:10px;margin-top:24px} 689 .rlcc-modal-btn{flex:1;appearance:none;border:1px solid transparent;border-radius:11px;min-height:44px;padding:0 18px;font:inherit;font-size:14px;font-weight:650;cursor:pointer;transition:background .15s ease,transform .1s ease,box-shadow .15s ease} 690 .rlcc-modal-btn:active{transform:scale(.98)} 691 .rlcc-modal-btn:focus-visible{outline:3px solid rgba(99,91,223,.3);outline-offset:2px} 692 .rlcc-modal-btn--ghost{background:#fff;border-color:#dfe3ed;color:#3b4560} 693 .rlcc-modal-btn--ghost:hover{background:#f4f5fa} 694 .rlcc-modal-btn--info{background:#635bdf;color:#fff} 695 .rlcc-modal-btn--info:hover{background:#5249cc} 696 .rlcc-modal-btn--warning{background:#d98200;color:#fff} 697 .rlcc-modal-btn--warning:hover{background:#bf7200} 698 .rlcc-modal-btn--danger{background:#c0392b;color:#fff} 699 .rlcc-modal-btn--danger:hover{background:#a63023} 700 @keyframes rlccFade{from{opacity:0}to{opacity:1}} 701 @keyframes rlccPop{from{opacity:0;transform:translateY(8px) scale(.96)}to{opacity:1;transform:none}} 702 @media(max-width:480px){.rlcc-modal{padding:24px 20px 20px}.rlcc-modal-actions{flex-direction:column-reverse}} 703 @media(prefers-reduced-motion:reduce){.rlcc-modal-overlay,.rlcc-modal{animation:none}} 704 705 /* Database Optimization tab: one row, three columns */ 706 .rlcc-card-grid--three{grid-template-columns:repeat(3,minmax(0,1fr));align-items:stretch} 707 .rlcc-card-grid--three .rlcc-card{display:flex;flex-direction:column} 708 .rlcc-card-grid--three .rlcc-grid{grid-template-columns:1fr} 709 .rlcc-card-grid--three .rlcc-db-grid{grid-template-columns:1fr;margin:0 0 18px} 710 .rlcc-card-grid--three .rlcc-clear-btn{align-self:flex-start} 711 .rlcc-card-grid--three .rlcc-hint{margin:14px 0 0} 712 .rlcc-card-grid--three .rlcc-danger-row{flex-direction:column;align-items:flex-start;gap:14px} 713 @media(max-width:1100px){.rlcc-card-grid--three{grid-template-columns:1fr}} -
cachenova/trunk/assets/js/cn-admin.js
r3737033 r3777263 36 36 const NS = "/" + RLCC_DATA.namespace; 37 37 const INTERVAL_LABELS = { 38 1: "1 minute",39 3: "3 minutes",40 5: "5 minutes",41 38 10: "10 minutes", 42 39 20: "20 minutes", 43 40 30: "30 minutes", 44 60: "1 hour" 41 60: "1 hour", 42 1440: "1 day", 43 10080: "7 days", 44 43200: "30 days (monthly)" 45 }; 46 const DB_SCHEDULE_LABELS = { 47 off: "Off (manual only)", 48 daily: "Every 1 day", 49 weekly: "Every 7 days", 50 monthly: "Every month" 45 51 }; 46 52 const OC_BACKEND_LABELS = { … … 64 70 if (mins < 60) return mins + " min ago"; 65 71 const hrs = Math.round(mins / 60); 66 return hrs + " hr ago"; 72 if (hrs < 24) return hrs + " hr ago"; 73 const days = Math.round(hrs / 24); 74 return days + (days === 1 ? " day ago" : " days ago"); 67 75 } 68 76 function InfoTip({text: text}) { … … 79 87 className: "screen-reader-text" 80 88 }, text)); 89 } 90 function formatDateTime(timestamp) { 91 if (!timestamp) return "—"; 92 try { 93 return new Date(timestamp * 1e3).toLocaleString(undefined, { 94 dateStyle: "medium", 95 timeStyle: "short" 96 }); 97 } catch (e) { 98 return new Date(timestamp * 1e3).toString(); 99 } 100 } 101 const MODAL_ICONS = { 102 danger: "!", 103 warning: "!", 104 info: "i" 105 }; 106 function ConfirmModal({modal: modal, onCancel: onCancel, onConfirm: onConfirm}) { 107 const confirmRef = useRef(null); 108 const cancelRef = useRef(null); 109 useEffect(() => { 110 if (!modal) return; 111 const previous = document.activeElement; 112 const target = "danger" === modal.tone ? cancelRef.current : confirmRef.current; 113 if (target) target.focus(); 114 function onKey(e) { 115 if (e.key === "Escape") { 116 e.preventDefault(); 117 onCancel(); 118 } else if (e.key === "Tab") { 119 const nodes = [ cancelRef.current, confirmRef.current ].filter(Boolean); 120 if (!nodes.length) return; 121 const first = nodes[0], last = nodes[nodes.length - 1]; 122 if (e.shiftKey && document.activeElement === first) { 123 e.preventDefault(); 124 last.focus(); 125 } else if (!e.shiftKey && document.activeElement === last) { 126 e.preventDefault(); 127 first.focus(); 128 } 129 } 130 } 131 document.addEventListener("keydown", onKey); 132 return () => { 133 document.removeEventListener("keydown", onKey); 134 if (previous && previous.focus) previous.focus(); 135 }; 136 }, [ modal ]); 137 if (!modal) return null; 138 const tone = modal.tone || "info"; 139 return h("div", { 140 className: "rlcc-modal-overlay", 141 onMouseDown: e => { 142 if (e.target === e.currentTarget) onCancel(); 143 } 144 }, h("div", { 145 className: "rlcc-modal rlcc-modal--" + tone, 146 role: "alertdialog", 147 "aria-modal": "true", 148 "aria-labelledby": "rlcc-modal-title", 149 "aria-describedby": "rlcc-modal-desc" 150 }, h("div", { 151 className: "rlcc-modal-icon", 152 "aria-hidden": "true" 153 }, MODAL_ICONS[tone] || "i"), h("h3", { 154 className: "rlcc-modal-title", 155 id: "rlcc-modal-title" 156 }, modal.title), h("p", { 157 className: "rlcc-modal-text", 158 id: "rlcc-modal-desc" 159 }, modal.message), modal.points && modal.points.length ? h("ul", { 160 className: "rlcc-modal-points" 161 }, modal.points.map((pt, i) => h("li", { 162 key: i 163 }, pt))) : null, h("div", { 164 className: "rlcc-modal-actions" 165 }, h("button", { 166 type: "button", 167 ref: cancelRef, 168 className: "rlcc-modal-btn rlcc-modal-btn--ghost", 169 onClick: onCancel 170 }, modal.cancelLabel || "Cancel"), h("button", { 171 type: "button", 172 ref: confirmRef, 173 className: "rlcc-modal-btn rlcc-modal-btn--" + tone, 174 onClick: onConfirm 175 }, modal.confirmLabel || "Continue")))); 81 176 } 82 177 class RLCCErrorBoundary extends Component { … … 158 253 optimize_tables: false 159 254 }); 255 const [modal, setModal] = useState(null); 256 function askConfirm(opts) { 257 setModal(opts); 258 } 259 function closeModal() { 260 setModal(null); 261 } 262 function acceptModal() { 263 const fn = modal && modal.onConfirm; 264 setModal(null); 265 if (fn) fn(); 266 } 267 const [activeTab, setActiveTab] = useState(window.location.hash === "#database" ? "database" : "cache"); 268 const [dbSchedule, setDbSchedule] = useState("off"); 269 const [dbNextRun, setDbNextRun] = useState(0); 270 const [dbLastType, setDbLastType] = useState("manual"); 271 const [dbScheduleSaving, setDbScheduleSaving] = useState(false); 160 272 const [dbRunning, setDbRunning] = useState(false); 161 273 const [dbLastReport, setDbLastReport] = useState(null); … … 183 295 const [localSslFixActive, setLocalSslFixActive] = useState(false); 184 296 const [diviActive, setDiviActive] = useState(false); 297 const [diviPerRequest, setDiviPerRequest] = useState(false); 298 const [diviPurgeSaving, setDiviPurgeSaving] = useState(false); 185 299 const [ecWpConfigWritable, setEcWpConfigWritable] = useState(true); 186 300 const [ecSaving, setEcSaving] = useState(false); … … 224 338 if (!toastEnabledRef.current) return; 225 339 const id = ++toastIdRef.current; 226 setToasts( prev => [ ...prev,{340 setToasts([{ 227 341 id: id, 228 342 message: message, … … 268 382 setLocalSslFixActive(!!res.local_ssl_fix_active); 269 383 setDiviActive(!!res.divi_active); 384 setDiviPerRequest(!!res.divi_per_request_purge); 270 385 setDbCounts(res.db_counts || { 271 386 revisions: 0, … … 275 390 expired_transients: 0 276 391 }); 392 setDbSchedule(res.db_schedule || "off"); 393 setDbNextRun(res.db_next_run_timestamp || 0); 277 394 if (res.last_db_optimize) { 278 395 setDbLastReport(res.last_db_optimize.report || null); 279 396 setDbLastTimestamp(res.last_db_optimize.timestamp || 0); 397 setDbLastType(res.last_db_optimize.type || "manual"); 280 398 } 281 399 setNextRun(res.next_run_timestamp); … … 387 505 function handleDeleteOnUninstallToggle() { 388 506 const newVal = !deleteOnUninstall; 389 if (newVal) { 390 const confirmed = window.confirm("When this is ON, deleting CacheNova from Plugins → Installed Plugins will permanently erase all of this plugin's settings and cached files, and trigger one final full-site cache clear. This cannot be undone. Turn it on?"); 391 if (!confirmed) { 392 return; 393 } 394 } 395 setDeleteOnUninstall(newVal); 396 deleteOnUninstallRef.current = newVal; 397 saveSettings({ 398 delete_data_on_uninstall: newVal 399 }, "Delete all data on uninstall turned " + (newVal ? "ON" : "OFF") + "."); 507 const apply = () => { 508 setDeleteOnUninstall(newVal); 509 deleteOnUninstallRef.current = newVal; 510 saveSettings({ 511 delete_data_on_uninstall: newVal 512 }, "Delete all data on uninstall turned " + (newVal ? "ON" : "OFF") + "."); 513 }; 514 if (!newVal) { 515 apply(); 516 return; 517 } 518 askConfirm({ 519 tone: "danger", 520 title: "Delete all data on uninstall?", 521 message: "When you delete CacheNova from Plugins → Installed Plugins, everything it created will be removed. This cannot be undone.", 522 points: [ "All CacheNova settings and reports", "All cached files", "One final full-site cache clear" ], 523 confirmLabel: "Yes, turn it on", 524 onConfirm: apply 525 }); 400 526 } 401 527 function saveObjectCacheSettings(overrides, onDone) { … … 448 574 const newVal = !ocEnabled; 449 575 if (newVal) { 450 const confirmed = window.confirm("This installs a wp-content/object-cache.php file that changes how WordPress caches data site-wide. This plugin automatically detects and uses the fastest backend this server actually has working — no manual setup needed. Continue?"); 451 if (!confirmed) { 452 return; 453 } 454 setOcTestStatus("testing"); 455 saveObjectCacheSettings({ 456 object_cache_enabled: newVal, 457 object_cache_backend: "auto" 576 askConfirm({ 577 tone: "info", 578 title: "Enable Persistent Object Cache?", 579 message: "CacheNova will install wp-content/object-cache.php, which changes how WordPress caches data across the whole site. It automatically picks the fastest backend your server has working.", 580 points: [ "Backend order: Redis, Memcached, APCu, then file-based", "No manual setup needed" ], 581 confirmLabel: "Enable", 582 onConfirm: () => { 583 setOcTestStatus("testing"); 584 saveObjectCacheSettings({ 585 object_cache_enabled: newVal, 586 object_cache_backend: "auto" 587 }); 588 } 458 589 }); 459 590 return; … … 483 614 return; 484 615 } 485 const confirmed = window.confirm("This installs a wp-content/advanced-cache.php file AND adds define('WP_CACHE', true) to wp-config.php, so cached pages can be served before WordPress even loads. Continue?"); 486 if (!confirmed) { 487 return; 488 } 489 } 616 askConfirm({ 617 tone: "info", 618 title: "Enable Early-Bypass Page Cache?", 619 message: "CacheNova will install wp-content/advanced-cache.php and add define('WP_CACHE', true) to wp-config.php, so cached pages are served before WordPress even loads.", 620 points: [ "Anonymous visitors only; logged-in users get Private Cache", "Removed automatically when you turn it off" ], 621 confirmLabel: "Enable", 622 onConfirm: () => runEarlyCache(newVal) 623 }); 624 return; 625 } 626 runEarlyCache(newVal); 627 } 628 function runEarlyCache(newVal) { 490 629 setEcSaving(true); 491 630 apiFetch({ … … 509 648 setLocalSslFixActive(!!res.local_ssl_fix_active); 510 649 setDiviActive(!!res.divi_active); 650 setDiviPerRequest(!!res.divi_per_request_purge); 511 651 const result = res.early_cache_result; 512 652 if (result) { … … 523 663 }); 524 664 } 665 function toggleDiviPerRequest() { 666 if (diviPurgeSaving) return; 667 const enabled = !diviPerRequest; 668 if (enabled) { 669 askConfirm({ 670 tone: "warning", 671 title: "Enable Divi cache compatibility?", 672 message: "CacheNova will delete the Divi et-cache folder after every frontend request. This can noticeably slow your website, so use it only for troubleshooting.", 673 confirmLabel: "Enable anyway", 674 onConfirm: () => runDiviPerRequest(enabled) 675 }); 676 return; 677 } 678 runDiviPerRequest(enabled); 679 } 680 function runDiviPerRequest(enabled) { 681 setDiviPurgeSaving(true); 682 apiFetch({path: NS + "/divi-per-request", method: "POST", data: {enabled}}).then(res => { 683 setDiviPerRequest(!!res.enabled); 684 pushToast(res.enabled ? (res.folder_deleted ? "Divi et-cache folder deleted. Automatic deletion enabled." : "Divi setting enabled, but et-cache folder could not be deleted. Check filesystem permissions.") : "Divi Cache Compatibility disabled. No per-request folder deletion.", res.enabled && !res.folder_deleted ? "error" : "success"); 685 }).catch(err => pushToast("Could not save Divi setting: " + (err.message || "Unknown error"), "error")) 686 .finally(() => setDiviPurgeSaving(false)); 687 } 688 function dbReportTotal(report) { 689 if (!report) return 0; 690 return Object.keys(report).filter(k => k !== "optimize_tables").reduce((sum, k) => sum + (typeof report[k] === "number" ? report[k] : 0), 0); 691 } 692 function switchTab(tab) { 693 setActiveTab(tab); 694 try { 695 window.history.replaceState(null, "", tab === "database" ? "#database" : window.location.pathname + window.location.search); 696 } catch (e) {} 697 } 698 function handleDbScheduleChange(e) { 699 const val = e.target.value; 700 if (val !== "off") { 701 askConfirm({ 702 tone: "danger", 703 title: "Turn on automatic optimization?", 704 message: "CacheNova will run " + (DB_SCHEDULE_LABELS[val] || val).toLowerCase() + " in the background. Every run permanently deletes the items below. This cannot be undone.", 705 points: [ "All post revisions and trashed posts", "Spam and trashed comments", "Expired transients, then optimizes tables" ], 706 confirmLabel: "Turn on", 707 onConfirm: () => runDbSchedule(val) 708 }); 709 return; 710 } 711 runDbSchedule(val); 712 } 713 function runDbSchedule(val) { 714 const previous = dbSchedule; 715 setDbSchedule(val); 716 setDbScheduleSaving(true); 717 apiFetch({ 718 path: NS + "/settings", 719 method: "POST", 720 data: { 721 interval_minutes: intervalRef.current, 722 toast_enabled: toastEnabledRef.current, 723 db_schedule: val 724 } 725 }).then(res => { 726 setDbScheduleSaving(false); 727 setDbSchedule(res.db_schedule || val); 728 setDbNextRun(res.db_next_run_timestamp || 0); 729 pushToast(val === "off" ? "Automatic database optimization turned OFF." : "Automatic database optimization set: " + (DB_SCHEDULE_LABELS[val] || val) + ".", "success"); 730 }).catch(err => { 731 setDbScheduleSaving(false); 732 setDbSchedule(previous); 733 const detail = err && (err.message || err.code) || "unknown error"; 734 pushToast("Failed to save database schedule: " + detail, "error"); 735 }); 736 } 525 737 function toggleDbOption(key) { 526 738 setDbOptions(prev => ({ … … 535 747 return; 536 748 } 537 const confirmed = window.confirm("This permanently deletes the selected items (revisions, trash, spam are not recoverable afterward). Continue?"); 538 if (!confirmed) { 539 return; 540 } 749 askConfirm({ 750 tone: "danger", 751 title: "Optimize database now?", 752 message: "The selected items will be permanently deleted. Revisions, trash and spam cannot be recovered afterward.", 753 confirmLabel: "Optimize now", 754 onConfirm: runDbOptimize 755 }); 756 } 757 function runDbOptimize() { 541 758 setDbRunning(true); 542 759 apiFetch({ … … 548 765 setDbLastReport(res.report); 549 766 setDbLastTimestamp(res.timestamp); 550 const total = Object.values(res.report).reduce((sum, n) => sum + (typeof n === "number" ? n : 0), 0); 767 setDbLastType("manual"); 768 const total = dbReportTotal(res.report); 551 769 pushToast("Database optimized — " + total + " item(s) cleaned.", "success"); 552 770 fetchSettings(false, false); … … 569 787 reminderFiredForRef.current = null; 570 788 setClearing(false); 571 pushToast( "Cache cleared successfully. Timer restarted.","success");789 pushToast(res.divi_folder_deleted === false ? (res.divi_folder_message || "Cache cleared, but Divi et-cache still exists.") : "All theme & plugin caches cleared. Timer restarted.", res.divi_folder_deleted === false ? "error" : "success"); 572 790 if (res.last_clear_report) { 573 791 setLastClearReport(res.last_clear_report); … … 626 844 }))); 627 845 } 846 function renderDbTab() { 847 const scheduleOn = dbSchedule !== "off"; 848 return h("div", { 849 className: "rlcc-card-grid rlcc-card-grid--three" 850 }, h("div", { 851 className: "rlcc-card" 852 }, h("h3", { 853 className: "rlcc-card-title" 854 }, h("span", { 855 className: "rlcc-card-symbol", 856 "aria-hidden": "true" 857 }, "◷"), "Automatic Optimization", h(InfoTip, { 858 text: "Runs in the background on WordPress cron. Each run clears ALL post revisions, trashed posts, spam comments, trashed comments and expired transients, then optimizes the database tables. Off by default." 859 })), h("div", { 860 className: "rlcc-grid" 861 }, h("div", { 862 className: "rlcc-field" 863 }, h("label", null, "Auto-optimize every"), h("select", { 864 value: dbSchedule, 865 onChange: handleDbScheduleChange, 866 disabled: dbScheduleSaving, 867 className: "rlcc-select" 868 }, Object.keys(DB_SCHEDULE_LABELS).map(k => h("option", { 869 key: k, 870 value: k 871 }, DB_SCHEDULE_LABELS[k])))), h("div", { 872 className: "rlcc-field" 873 }, h("label", null, "Status"), h("div", { 874 className: "rlcc-db-status " + (scheduleOn ? "rlcc-db-status--on" : "rlcc-db-status--off") 875 }, scheduleOn ? "Active — next run " + formatDateTime(dbNextRun) : "Off — nothing runs automatically")))), h("div", { 876 className: "rlcc-card" 877 }, h("h3", { 878 className: "rlcc-card-title" 879 }, h("span", { 880 className: "rlcc-card-symbol", 881 "aria-hidden": "true" 882 }, "▤"), "Manual Optimization", h(InfoTip, { 883 text: "Cleans up post revisions, trashed items, spam comments, and expired transients, and defragments your database tables. Nothing runs until you click the button below, and only for the items you've checked." 884 })), h("div", { 885 className: "rlcc-db-grid" 886 }, [ [ "revisions", "Post Revisions" ], [ "trashed_posts", "Trashed Posts" ], [ "spam_comments", "Spam Comments" ], [ "trashed_comments", "Trashed Comments" ], [ "expired_transients", "Expired Transients" ] ].map(([key, label]) => h("label", { 887 key: key, 888 className: "rlcc-db-checkbox" 889 }, h("input", { 890 type: "checkbox", 891 checked: !!dbOptions[key], 892 onChange: () => toggleDbOption(key) 893 }), h("span", null, label), h("span", { 894 className: "rlcc-db-count" 895 }, dbCounts[key] || 0))), h("label", { 896 className: "rlcc-db-checkbox" 897 }, h("input", { 898 type: "checkbox", 899 checked: !!dbOptions.optimize_tables, 900 onChange: () => toggleDbOption("optimize_tables") 901 }), h("span", null, "Optimize Database Tables"))), h("button", { 902 className: "rlcc-clear-btn", 903 onClick: handleDbOptimize, 904 disabled: dbRunning, 905 type: "button" 906 }, dbRunning ? "Optimizing…" : "Optimize Database Now"), dbLastTimestamp > 0 && h("p", { 907 className: "rlcc-hint" 908 }, "Last run: ", timeAgo(dbLastTimestamp), " (" + (dbLastType === "auto" ? "automatic" : "manual") + ")", dbLastReport ? " — " + dbReportTotal(dbLastReport) + " item(s) cleaned" + (dbLastReport.optimize_tables ? ", " + dbLastReport.optimize_tables + " table(s) optimized" : "") : "")), h("div", { 909 className: "rlcc-card rlcc-card--danger" 910 }, h("h3", { 911 className: "rlcc-card-title rlcc-card-title--danger" 912 }, "Danger Zone"), h("div", { 913 className: "rlcc-danger-row" 914 }, h("div", { 915 className: "rlcc-danger-text" 916 }, h("label", null, "Delete all data on uninstall", h(InfoTip, { 917 text: "Off by default. When ON, removing this plugin from Plugins → Installed Plugins deletes all of its settings and cached files, and runs one last full-site cache clear in the background." 918 }))), h("button", { 919 className: "rlcc-switch rlcc-switch--danger " + (deleteOnUninstall ? "rlcc-switch--on" : "rlcc-switch--off"), 920 onClick: handleDeleteOnUninstallToggle, 921 type: "button" 922 }, h("span", { 923 className: "rlcc-switch-knob" 924 }), h("span", { 925 className: "rlcc-switch-label" 926 }, deleteOnUninstall ? "On" : "Off"))))); 927 } 628 928 if (loading) { 629 929 return h("div", { … … 641 941 }, h("h2", { 642 942 className: "rlcc-hero-title" 643 }, "CacheNova"), h("div", {943 }, h("span", {className:"rlcc-brand-mark", "aria-hidden":"true"}, "✦"), "CacheNova", h("span", {className:"rlcc-version"}, "v1.1.0")), h("div", { 644 944 className: "rlcc-status-pill" 645 945 }, h("span", { … … 659 959 }, "Last cleared: ", h("strong", null, timeAgo(lastCleared))), diviActive ? h("div", { 660 960 className: "rlcc-hero-meta-item rlcc-hero-meta-item--divi" 661 }, " 🛡 Divi detected — et-cache folder purged continuously") : h("div", {961 }, "Divi detected — cache cleared on updates and scheduled runs") : h("div", { 662 962 className: "rlcc-hero-meta-item" 663 }, "✓ Works with any theme and plugin"))), h("div", { 963 }, "Works with WordPress themes — automatic cache clearing enabled"))), h("div", { 964 className: "rlcc-tabs", 965 role: "tablist" 966 }, [ [ "cache", "Cache Settings" ], [ "database", "Database Optimization" ] ].map(([key, label]) => h("button", { 967 key: key, 968 type: "button", 969 role: "tab", 970 "aria-selected": activeTab === key, 971 className: "rlcc-tab" + (activeTab === key ? " rlcc-tab--active" : ""), 972 onClick: () => switchTab(key) 973 }, label))), diviActive && activeTab === "cache" && h("div", {className:"rlcc-card rlcc-divi-compat"}, 974 h("h3", {className:"rlcc-card-title"}, "Divi Cache Compatibility"), 975 h("p", null, "Delete the entire wp-content/et-cache folder immediately when enabled and after frontend requests. Divi may recreate it. Can significantly slow your site."), 976 h("button", {type:"button", className:"rlcc-switch " + (diviPerRequest ? "rlcc-switch--on" : "rlcc-switch--off"), onClick:toggleDiviPerRequest, disabled:diviPurgeSaving, "aria-pressed":diviPerRequest}, 977 h("span", {className:"rlcc-switch-knob"}), h("span", {className:"rlcc-switch-label"}, diviPerRequest ? "On" : "Off")) 978 ), activeTab === "database" ? renderDbTab() : h("div", { 664 979 className: "rlcc-card-grid" 665 980 }, h("div", { … … 667 982 }, h("h3", { 668 983 className: "rlcc-card-title" 669 }, "Schedule & Notifications", h(InfoTip, {984 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "◷"), "Schedule & Notifications", h(InfoTip, { 670 985 text: "Cache also clears instantly every time you save a page or post — Divi Builder, Gutenberg, or any other editor — no manual reload needed." 671 986 })), h("div", { … … 695 1010 }, h("h3", { 696 1011 className: "rlcc-card-title" 697 }, "Cache Types", h(InfoTip, {1012 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "▦"), "Cache Types", h(InfoTip, { 698 1013 text: "Whatever you switch ON here gets stored; the timer above is what clears it back out again." 699 1014 })), h("div", { … … 740 1055 className: "rlcc-switch-label" 741 1056 }, cacheBrowser ? "On" : "Off"))))), h("div", { 742 className: "rlcc-card rlcc-card--wide"743 }, h("h3", {744 className: "rlcc-card-title"745 }, "Last Clear Report"), h(ClearReportCard)), h("div", {746 1057 className: "rlcc-card" 747 1058 }, h("h3", { 748 1059 className: "rlcc-card-title" 749 }, "Persistent Object Cache", h(InfoTip, {1060 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "◇"), "Persistent Object Cache", h(InfoTip, { 750 1061 text: "A persistent object cache lets WordPress reuse database query results across requests. Off by default. Turning it on automatically detects and uses the fastest backend this server actually has working — Redis, then Memcached, then APCu, then a universal file-based fallback — with no manual picking or testing required." 751 1062 })), ocForeignDropin && h("p", { 752 1063 className: "rlcc-oc-unavailable" 753 }, "A different object-cache.php already exists in wp-content/ — remove it first if you want this plugin to manage the persistent object cache."), h("div", {1064 }, "An existing object-cache.php is already installed (possibly by the host or an inactive plugin). CacheNova will not overwrite it. Identify its owner and back it up before switching cache providers."), h("div", { 754 1065 className: "rlcc-field" 755 1066 }, h("label", null, "Enable (" + (ocDropinActive ? "drop-in active" : "not active") + ")"), h("button", { … … 823 1134 }, h("h3", { 824 1135 className: "rlcc-card-title" 825 }, "Early-Bypass Page Cache", h(InfoTip, {1136 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "⚡"), "Early-Bypass Page Cache", h(InfoTip, { 826 1137 text: "Serves cached pages before WordPress even loads. Only anonymous, cookie-free visitors; everyone else gets Private Cache instead. This is what actually improves server response time (TTFB) on speed tests." 827 1138 })), ecForeignDropin && h("p", { 828 1139 className: "rlcc-oc-unavailable" 829 }, "A different advanced-cache.php already exists in wp-content/ — remove it first if you want this plugin to manage early-bypass caching."), !cachePublic && !ecEnabled && h("p", {1140 }, "An existing advanced-cache.php is already installed (possibly by the host or an inactive plugin). CacheNova will not overwrite it. Identify its owner and back it up before switching cache providers."), !cachePublic && !ecEnabled && h("p", { 830 1141 className: "rlcc-oc-unavailable" 831 1142 }, "Turn on Public Cache first — early-bypass caching serves the same cached files, just before WordPress loads."), h("div", { … … 848 1159 }, h("h3", { 849 1160 className: "rlcc-card-title" 850 }, "Database Optimization", h(InfoTip, { 851 text: "Cleans up post revisions, trashed items, spam comments, and expired transients, and defragments your database tables. Off by default — nothing runs until you click the button below, and only for the items you've checked." 852 })), h("div", { 853 className: "rlcc-db-grid" 854 }, [ [ "revisions", "Post Revisions" ], [ "trashed_posts", "Trashed Posts" ], [ "spam_comments", "Spam Comments" ], [ "trashed_comments", "Trashed Comments" ], [ "expired_transients", "Expired Transients" ] ].map(([key, label]) => h("label", { 855 key: key, 856 className: "rlcc-db-checkbox" 857 }, h("input", { 858 type: "checkbox", 859 checked: !!dbOptions[key], 860 onChange: () => toggleDbOption(key) 861 }), h("span", null, label), h("span", { 862 className: "rlcc-db-count" 863 }, dbCounts[key] || 0))), h("label", { 864 className: "rlcc-db-checkbox" 865 }, h("input", { 866 type: "checkbox", 867 checked: !!dbOptions.optimize_tables, 868 onChange: () => toggleDbOption("optimize_tables") 869 }), h("span", null, "Optimize Database Tables"))), h("button", { 870 className: "rlcc-clear-btn", 871 onClick: handleDbOptimize, 872 disabled: dbRunning, 873 type: "button" 874 }, dbRunning ? "Optimizing…" : "Optimize Database Now"), dbLastTimestamp > 0 && h("p", { 875 className: "rlcc-hint" 876 }, "Last run: ", timeAgo(dbLastTimestamp), dbLastReport ? " — " + Object.values(dbLastReport).reduce((sum, n) => sum + (typeof n === "number" ? n : 0), 0) + " item(s) cleaned" : "")), h("div", { 877 className: "rlcc-card rlcc-card--wide rlcc-card--danger" 878 }, h("h3", { 879 className: "rlcc-card-title rlcc-card-title--danger" 880 }, "Danger Zone"), h("div", { 881 className: "rlcc-danger-row" 882 }, h("div", { 883 className: "rlcc-danger-text" 884 }, h("label", null, "Delete all data on uninstall", h(InfoTip, { 885 text: "Off by default. When ON, removing this plugin from Plugins → Installed Plugins deletes all of its settings and cached files, and runs one last full-site cache clear in the background." 886 }))), h("button", { 887 className: "rlcc-switch rlcc-switch--danger " + (deleteOnUninstall ? "rlcc-switch--on" : "rlcc-switch--off"), 888 onClick: handleDeleteOnUninstallToggle, 889 type: "button" 890 }, h("span", { 891 className: "rlcc-switch-knob" 892 }), h("span", { 893 className: "rlcc-switch-label" 894 }, deleteOnUninstall ? "On" : "Off"))))), h(ToastList, { 1161 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "▤"), "Last Clear Report"), h(ClearReportCard))), h(ConfirmModal, { 1162 modal: modal, 1163 onCancel: closeModal, 1164 onConfirm: acceptModal 1165 }), h(ToastList, { 895 1166 toasts: toasts, 896 1167 remove: removeToast -
cachenova/trunk/assets/js/cn-admin.min.js
r3737033 r3777263 1 (function(){function onReady(fn){if("loading"===document.readyState){document.addEventListener("DOMContentLoaded",fn)}else{fn()}}function renderFatal(title,detail){const root=document.getElementById("rlcc-settings-root");if(!root)return;root.innerHTML="";const wrap=document.createElement("div");wrap.className="rlcc-fatal-error";wrap.style.cssText='max-width:820px;margin:20px 0;padding:16px 20px;border:1px solid #e4e6eb;border-left:4px solid #c0392b;border-radius:6px;background:#fdf3f2;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';const h4=document.createElement("p");h4.style.cssText="margin:0 0 6px;font-weight:600;color:#c0392b;";h4.textContent="CacheNova — "+title;const p=document.createElement("p");p.style.cssText="margin:0;color:#3c3c3c;white-space:pre-wrap;word-break:break-word;";p.textContent=detail;wrap.appendChild(h4);wrap.appendChild(p);root.appendChild(wrap)}if(typeof wp==="undefined"||!wp.element||!wp.apiFetch){onReady(function(){renderFatal("dashboard could not start","A required WordPress script (wp-element or wp-api-fetch) did not load on this page, most likely because another plugin or theme script on this screen errored first and stopped the rest of the queue. Open your browser console (F12) on this page for the first red error, or try disabling other plugins one at a time to find the conflict.")});return}const{createElement:h,useState:useState,useEffect:useEffect,useRef:useRef,Component:Component}=wp.element;const apiFetch=wp.apiFetch;apiFetch.use(apiFetch.createNonceMiddleware(RLCC_DATA.nonce));apiFetch.use(apiFetch.createRootURLMiddleware(RLCC_DATA.restUrl));const NS="/"+RLCC_DATA.namespace;const INTERVAL_LABELS={1:"1 minute",3:"3 minutes",5:"5 minutes",10:"10 minutes",20:"20 minutes",30:"30 minutes",60:"1 hour"};const OC_BACKEND_LABELS={redis:"Redis",memcached:"Memcache",apcu:"APCu",file:"File-based (universal fallback)"};function formatCountdown(seconds){if(seconds===null)return"--:--";if(seconds<=0)return"0:00";const m=Math.floor(seconds/60);const s=seconds%60;return m+":"+String(s).padStart(2,"0")}function timeAgo(timestamp){if(!timestamp)return"never";const seconds=Math.floor(Date.now()/1e3)-timestamp;if(seconds<60)return"just now";const mins=Math.round(seconds/60);if(mins<60)return mins+" min ago";const hrs=Math.round(mins/60);return hrs+" hr ago"}function InfoTip({text:text}){return h("span",{className:"rlcc-infotip",tabIndex:0},h("span",{className:"rlcc-infotip-icon","aria-hidden":"true"},"i"),h("span",{className:"rlcc-infotip-text",role:"tooltip"},text),h("span",{className:"screen-reader-text"},text))}class RLCCErrorBoundary extends Component{constructor(props){super(props);this.state={error:null}}static getDerivedStateFromError(error){return{error:error}}render(){if(this.state.error){return h("div",{className:"rlcc-fatal-error",style:{maxWidth:"820px",margin:"20px 0",padding:"16px 20px",border:"1px solid #e4e6eb",borderLeft:"4px solid #c0392b",borderRadius:"6px",background:"#fdf3f2"}},h("p",{style:{margin:"0 0 6px",fontWeight:600,color:"#c0392b"}},"CacheNova — the dashboard hit an error"),h("p",{style:{margin:0,color:"#3c3c3c",whiteSpace:"pre-wrap",wordBreak:"break-word"}},String(this.state.error&&this.state.error.message||this.state.error)))}return this.props.children}}function ToastList({toasts:toasts,remove:remove}){return h("div",{className:"rlcc-toast-container"},toasts.map(t=>h("div",{key:t.id,className:"rlcc-toast rlcc-toast--"+t.type,onClick:()=>remove(t.id)},h("span",{className:"rlcc-toast-icon"},t.type==="success"?"✓":t.type==="error"?"✕":"ℹ"),h("span",null,t.message))))}function App(){const[loading,setLoading]=useState(true);const[interval_,setInterval_]=useState(5);const[toastEnabled,setToastEnabled]=useState(true);const[cachePublic,setCachePublic]=useState(true);const[cachePrivate,setCachePrivate]=useState(true);const[cacheObject,setCacheObject]=useState(false);const[cacheBrowser,setCacheBrowser]=useState(false);const[deleteOnUninstall,setDeleteOnUninstall]=useState(false);const[dbCounts,setDbCounts]=useState({revisions:0,trashed_posts:0,spam_comments:0,trashed_comments:0,expired_transients:0});const[dbOptions,setDbOptions]=useState({revisions:true,trashed_posts:true,spam_comments:true,trashed_comments:true,expired_transients:true,optimize_tables:false});const[dbRunning,setDbRunning]=useState(false);const[dbLastReport,setDbLastReport]=useState(null);const[dbLastTimestamp,setDbLastTimestamp]=useState(0);const[ocEnabled,setOcEnabled]=useState(false);const[ocBackend,setOcBackend]=useState("apcu");const[ocRedisHost,setOcRedisHost]=useState("127.0.0.1");const[ocRedisPort,setOcRedisPort]=useState(6379);const[ocMemcachedHost,setOcMemcachedHost]=useState("127.0.0.1");const[ocMemcachedPort,setOcMemcachedPort]=useState(11211);const[ocAvailable,setOcAvailable]=useState({apcu:false,redis:false,memcached:false});const[ocDropinActive,setOcDropinActive]=useState(false);const[ocForeignDropin,setOcForeignDropin]=useState(false);const[ocTestStatus,setOcTestStatus]=useState("idle");const[ocTestMessage,setOcTestMessage]=useState("");const[ocSaving,setOcSaving]=useState(false);const[ecEnabled,setEcEnabled]=useState(false);const[ecDropinActive,setEcDropinActive]=useState(false);const[ecForeignDropin,setEcForeignDropin]=useState(false);const[ecWpCacheTrue,setEcWpCacheTrue]=useState(false);const[localSslFixActive,setLocalSslFixActive]=useState(false);const[diviActive,setDiviActive]=useState(false);const[ecWpConfigWritable,setEcWpConfigWritable]=useState(true);const[ecSaving,setEcSaving]=useState(false);const[ecResultStatus,setEcResultStatus]=useState("idle");const[ecResultMessage,setEcResultMessage]=useState("");const[nextRun,setNextRun]=useState(0);const[lastCleared,setLastCleared]=useState(0);const[lastClearReport,setLastClearReport]=useState(null);const[secondsLeft,setSecondsLeft]=useState(null);const[saving,setSaving]=useState(false);const[clearing,setClearing]=useState(false);const[toasts,setToasts]=useState([]);const toastIdRef=useRef(0);const toastEnabledRef=useRef(toastEnabled);toastEnabledRef.current=toastEnabled;const lastNotifiedClearRef=useRef(0);const reminderFiredForRef=useRef(null);const intervalRef=useRef(interval_);intervalRef.current=interval_;const cachePublicRef=useRef(cachePublic);cachePublicRef.current=cachePublic;const cachePrivateRef=useRef(cachePrivate);cachePrivateRef.current=cachePrivate;const cacheObjectRef=useRef(cacheObject);cacheObjectRef.current=cacheObject;const cacheBrowserRef=useRef(cacheBrowser);cacheBrowserRef.current=cacheBrowser;const deleteOnUninstallRef=useRef(deleteOnUninstall);deleteOnUninstallRef.current=deleteOnUninstall;const ocBackendRef=useRef(ocBackend);ocBackendRef.current=ocBackend;const ocRedisHostRef=useRef(ocRedisHost);ocRedisHostRef.current=ocRedisHost;const ocRedisPortRef=useRef(ocRedisPort);ocRedisPortRef.current=ocRedisPort;const ocMemcachedHostRef=useRef(ocMemcachedHost);ocMemcachedHostRef.current=ocMemcachedHost;const ocMemcachedPortRef=useRef(ocMemcachedPort);ocMemcachedPortRef.current=ocMemcachedPort;function pushToast(message,type){if(!toastEnabledRef.current)return;const id=++toastIdRef.current;setToasts(prev=>[...prev,{id:id,message:message,type:type}]);setTimeout(()=>{setToasts(prev=>prev.filter(t=>t.id!==id))},3500)}function removeToast(id){setToasts(prev=>prev.filter(t=>t.id!==id))}function fetchSettings(showErrorToast,isBackgroundPoll){return apiFetch({path:NS+"/settings",method:"GET"}).then(res=>{setInterval_(res.interval_minutes);setToastEnabled(!!res.toast_enabled);setCachePublic(!!res.cache_public);setCachePrivate(!!res.cache_private);setCacheObject(!!res.cache_object);setCacheBrowser(!!res.cache_browser);setDeleteOnUninstall(!!res.delete_data_on_uninstall);setOcEnabled(!!res.object_cache_enabled);setOcBackend(res.object_cache_backend||"apcu");setOcRedisHost(res.object_cache_redis_host||"127.0.0.1");setOcRedisPort(res.object_cache_redis_port||6379);setOcMemcachedHost(res.object_cache_memcached_host||"127.0.0.1");setOcMemcachedPort(res.object_cache_memcached_port||11211);setOcAvailable(res.object_cache_available_backends||{apcu:false,redis:false,memcached:false});setOcDropinActive(!!res.object_cache_dropin_active);setOcForeignDropin(!!res.object_cache_foreign_dropin);setEcEnabled(!!res.early_cache_enabled);setEcDropinActive(!!res.advanced_cache_dropin_active);setEcForeignDropin(!!res.advanced_cache_foreign_dropin);setEcWpCacheTrue(!!res.wp_cache_constant_true);setEcWpConfigWritable(res.wp_config_writable!==false);setLocalSslFixActive(!!res.local_ssl_fix_active);setDiviActive(!!res.divi_active);setDbCounts(res.db_counts||{revisions:0,trashed_posts:0,spam_comments:0,trashed_comments:0,expired_transients:0});if(res.last_db_optimize){setDbLastReport(res.last_db_optimize.report||null);setDbLastTimestamp(res.last_db_optimize.timestamp||0)}setNextRun(res.next_run_timestamp);if(isBackgroundPoll&&res.last_cleared_timestamp&&res.last_cleared_timestamp!==lastNotifiedClearRef.current){lastNotifiedClearRef.current=res.last_cleared_timestamp;pushToast("Cache auto-cleared in the background.","success")}else if(!isBackgroundPoll){lastNotifiedClearRef.current=res.last_cleared_timestamp}setLastCleared(res.last_cleared_timestamp);setLastClearReport(res.last_clear_report||null);setLoading(false)}).catch(err=>{setLoading(false);if(showErrorToast){const detail=err&&(err.message||err.code)||"unknown error";pushToast("Could not load settings: "+detail,"error")}})}useEffect(()=>{fetchSettings(true,false)},[]);useEffect(()=>{if(!nextRun){setSecondsLeft(null);return}if(reminderFiredForRef.current!==nextRun){reminderFiredForRef.current=null}function tick(){const remaining=nextRun-Math.floor(Date.now()/1e3);const clamped=Math.max(0,remaining);setSecondsLeft(clamped);const totalSeconds=intervalRef.current*60;const halfPoint=Math.floor(totalSeconds/2);if(clamped<=halfPoint&&clamped>0&&reminderFiredForRef.current!==nextRun&&totalSeconds>=120){reminderFiredForRef.current=nextRun;const mins=Math.round(clamped/60);const label=mins>=1?mins+" min":clamped+" sec";pushToast(label+" remaining until next cache clear.","info")}}tick();const id=window.setInterval(tick,1e3);return()=>window.clearInterval(id)},[nextRun]);useEffect(()=>{const id=window.setInterval(()=>fetchSettings(false,true),15e3);return()=>window.clearInterval(id)},[]);function saveSettings(overrides,successMessage){const newInterval="interval_minutes"in overrides?overrides.interval_minutes:intervalRef.current;setSaving(true);apiFetch({path:NS+"/settings",method:"POST",data:{interval_minutes:newInterval,toast_enabled:"toast_enabled"in overrides?overrides.toast_enabled:toastEnabledRef.current,cache_public:"cache_public"in overrides?overrides.cache_public:cachePublicRef.current,cache_private:"cache_private"in overrides?overrides.cache_private:cachePrivateRef.current,cache_object:"cache_object"in overrides?overrides.cache_object:cacheObjectRef.current,cache_browser:"cache_browser"in overrides?overrides.cache_browser:cacheBrowserRef.current,delete_data_on_uninstall:"delete_data_on_uninstall"in overrides?overrides.delete_data_on_uninstall:deleteOnUninstallRef.current}}).then(res=>{setNextRun(res.next_run_timestamp);setSaving(false);pushToast(successMessage||"Settings saved.","success")}).catch(err=>{setSaving(false);const detail=err&&(err.message||err.code)||"unknown error";pushToast("Failed to save settings: "+detail,"error");console.error("ACC save settings error:",err)})}function handleIntervalChange(e){const val=parseInt(e.target.value,10);setInterval_(val);saveSettings({interval_minutes:val},"Settings saved — auto-clear every "+(INTERVAL_LABELS[val]||val+" min")+".")}function handleToastToggle(){const newVal=!toastEnabled;setToastEnabled(newVal);toastEnabledRef.current=newVal;saveSettings({toast_enabled:newVal})}function handleCacheToggle(key){const setters={cache_public:[cachePublic,setCachePublic,cachePublicRef,"Public Cache"],cache_private:[cachePrivate,setCachePrivate,cachePrivateRef,"Private Cache"],cache_object:[cacheObject,setCacheObject,cacheObjectRef,"Object Cache"],cache_browser:[cacheBrowser,setCacheBrowser,cacheBrowserRef,"Browser Cache"]};const[current,setState,ref,label]=setters[key];const newVal=!current;setState(newVal);ref.current=newVal;saveSettings({[key]:newVal},label+" turned "+(newVal?"ON":"OFF")+".")}function handleDeleteOnUninstallToggle(){const newVal=!deleteOnUninstall;if(newVal){const confirmed=window.confirm("When this is ON, deleting CacheNova from Plugins → Installed Plugins will permanently erase all of this plugin's settings and cached files, and trigger one final full-site cache clear. This cannot be undone. Turn it on?");if(!confirmed){return}}setDeleteOnUninstall(newVal);deleteOnUninstallRef.current=newVal;saveSettings({delete_data_on_uninstall:newVal},"Delete all data on uninstall turned "+(newVal?"ON":"OFF")+".")}function saveObjectCacheSettings(overrides,onDone){setOcSaving(true);apiFetch({path:NS+"/settings",method:"POST",data:{interval_minutes:intervalRef.current,toast_enabled:toastEnabledRef.current,cache_public:cachePublicRef.current,cache_private:cachePrivateRef.current,cache_object:cacheObjectRef.current,cache_browser:cacheBrowserRef.current,delete_data_on_uninstall:deleteOnUninstallRef.current,object_cache_backend:ocBackendRef.current,object_cache_redis_host:ocRedisHostRef.current,object_cache_redis_port:ocRedisPortRef.current,object_cache_memcached_host:ocMemcachedHostRef.current,object_cache_memcached_port:ocMemcachedPortRef.current,...overrides}}).then(res=>{setOcSaving(false);setOcEnabled(!!res.object_cache_enabled);setOcBackend(res.object_cache_backend||ocBackendRef.current);setOcDropinActive(!!res.object_cache_dropin_active);const result=res.object_cache_result;if(result){if(result.success){setOcTestStatus("ok");setOcTestMessage(result.message);pushToast(result.message,"success")}else{setOcTestStatus("error");setOcTestMessage(result.message);pushToast(result.message,"error")}}else{pushToast("Object cache settings saved.","success")}if(onDone)onDone(result)}).catch(err=>{setOcSaving(false);const detail=err&&(err.message||err.code)||"unknown error";pushToast("Failed to save object cache settings: "+detail,"error")})}function handleOcToggle(){const newVal=!ocEnabled;if(newVal){const confirmed=window.confirm("This installs a wp-content/object-cache.php file that changes how WordPress caches data site-wide. This plugin automatically detects and uses the fastest backend this server actually has working — no manual setup needed. Continue?");if(!confirmed){return}setOcTestStatus("testing");saveObjectCacheSettings({object_cache_enabled:newVal,object_cache_backend:"auto"});return}saveObjectCacheSettings({object_cache_enabled:newVal})}function handleOcRedetect(){setOcTestStatus("testing");saveObjectCacheSettings({object_cache_enabled:true,object_cache_backend:"auto"})}function handleOcFieldCommit(){setOcTestStatus("idle");if(ocEnabled){saveObjectCacheSettings({})}}function handleEarlyCacheToggle(){const newVal=!ecEnabled;if(newVal){if(!cachePublic){pushToast("Turn on Public Cache first — early-bypass caching serves the same cached files, just before WordPress loads.","error");return}const confirmed=window.confirm("This installs a wp-content/advanced-cache.php file AND adds define('WP_CACHE', true) to wp-config.php, so cached pages can be served before WordPress even loads. Continue?");if(!confirmed){return}}setEcSaving(true);apiFetch({path:NS+"/settings",method:"POST",data:{interval_minutes:intervalRef.current,toast_enabled:toastEnabledRef.current,cache_public:cachePublicRef.current,cache_private:cachePrivateRef.current,cache_object:cacheObjectRef.current,cache_browser:cacheBrowserRef.current,delete_data_on_uninstall:deleteOnUninstallRef.current,early_cache_enabled:newVal}}).then(res=>{setEcSaving(false);setEcEnabled(!!res.early_cache_enabled);setEcDropinActive(!!res.advanced_cache_dropin_active);setEcWpCacheTrue(!!res.wp_cache_constant_true);setLocalSslFixActive(!!res.local_ssl_fix_active);setDiviActive(!!res.divi_active);const result=res.early_cache_result;if(result){setEcResultStatus(result.success?"ok":"error");setEcResultMessage(result.message);pushToast(result.message,result.success?"success":"error")}else{pushToast("Early-bypass cache settings saved.","success")}}).catch(err=>{setEcSaving(false);const detail=err&&(err.message||err.code)||"unknown error";pushToast("Failed to save early-bypass cache settings: "+detail,"error")})}function toggleDbOption(key){setDbOptions(prev=>({...prev,[key]:!prev[key]}))}function handleDbOptimize(){const selected=Object.keys(dbOptions).filter(k=>dbOptions[k]);if(!selected.length){pushToast("Select at least one item to optimize.","error");return}const confirmed=window.confirm("This permanently deletes the selected items (revisions, trash, spam are not recoverable afterward). Continue?");if(!confirmed){return}setDbRunning(true);apiFetch({path:NS+"/database/optimize",method:"POST",data:dbOptions}).then(res=>{setDbRunning(false);setDbLastReport(res.report);setDbLastTimestamp(res.timestamp);const total=Object.values(res.report).reduce((sum,n)=>sum+(typeof n==="number"?n:0),0);pushToast("Database optimized — "+total+" item(s) cleaned.","success");fetchSettings(false,false)}).catch(err=>{setDbRunning(false);const detail=err&&(err.message||err.code)||"unknown error";pushToast("Database optimization failed: "+detail,"error");console.error("ACC db optimize error:",err)})}function handleClearNow(){setClearing(true);apiFetch({path:NS+"/clear",method:"POST"}).then(res=>{setLastCleared(res.last_cleared_timestamp);lastNotifiedClearRef.current=res.last_cleared_timestamp;setNextRun(res.next_run_timestamp);reminderFiredForRef.current=null;setClearing(false);pushToast("Cache cleared successfully. Timer restarted.","success");if(res.last_clear_report){setLastClearReport(res.last_clear_report)}else{setTimeout(()=>fetchSettings(false,true),1500)}}).catch(err=>{setClearing(false);const detail=err&&(err.message||err.code)||"unknown error";pushToast("Failed to clear cache: "+detail,"error");console.error("ACC clear cache error:",err)})}const REPORT_STATUS_META={ok:{icon:"✓",text:"Verified",className:"rlcc-report-ok"},triggered:{icon:"↻",text:"Triggered",className:"rlcc-report-triggered"},failed:{icon:"✕",text:"Failed",className:"rlcc-report-failed"}};function ClearReportCard(){if(!lastClearReport||!lastClearReport.items||!Object.keys(lastClearReport.items).length){return h("p",{className:"rlcc-hint"},'No cache clear has run yet since this page loaded — trigger one with "Clear Cache Now" or wait for the scheduled timer.')}const items=lastClearReport.items;const failedCount=Object.values(items).filter(i=>i.status==="failed").length;return h("div",null,h("p",{className:"rlcc-hint"},("full"===lastClearReport.type?"Full clear":"Scheduled (light) clear")+" — "+(failedCount?failedCount+" layer(s) failed to verify.":"every checked layer verified successfully.")),h("ul",{className:"rlcc-report-list"},Object.keys(items).map(key=>{const item=items[key];const meta=REPORT_STATUS_META[item.status]||REPORT_STATUS_META.triggered;return h("li",{key:key,className:"rlcc-report-row "+meta.className},h("span",{className:"rlcc-report-icon"},meta.icon),h("span",{className:"rlcc-report-label"},item.label),h("span",{className:"rlcc-report-badge"},meta.text))})))}if(loading){return h("div",{className:"rlcc-dashboard-loading"},"Loading settings…")}return h("div",{className:"rlcc-dashboard"},h("div",{className:"rlcc-hero"},h("div",{className:"rlcc-hero-top"},h("div",{className:"rlcc-hero-title-block"},h("h2",{className:"rlcc-hero-title"},"CacheNova"),h("div",{className:"rlcc-status-pill"},h("span",{className:"rlcc-status-dot"}),h("span",null,"Running in background"))),h("button",{className:"rlcc-clear-btn",onClick:handleClearNow,disabled:clearing},clearing?"Clearing…":"Clear Cache Now")),h("div",{className:"rlcc-hero-meta"},h("div",{className:"rlcc-countdown-row"},h("span",null,"Next automatic clear in"),h("span",{className:"rlcc-countdown"},formatCountdown(secondsLeft))),h("div",{className:"rlcc-hero-meta-item"},"Last cleared: ",h("strong",null,timeAgo(lastCleared))),diviActive?h("div",{className:"rlcc-hero-meta-item rlcc-hero-meta-item--divi"},"🛡 Divi detected — et-cache folder purged continuously"):h("div",{className:"rlcc-hero-meta-item"},"✓ Works with any theme and plugin"))),h("div",{className:"rlcc-card-grid"},h("div",{className:"rlcc-card"},h("h3",{className:"rlcc-card-title"},"Schedule & Notifications",h(InfoTip,{text:"Cache also clears instantly every time you save a page or post — Divi Builder, Gutenberg, or any other editor — no manual reload needed."})),h("div",{className:"rlcc-grid"},h("div",{className:"rlcc-field"},h("label",null,"Auto-clear every"),h("select",{value:interval_,onChange:handleIntervalChange,disabled:saving,className:"rlcc-select"},RLCC_DATA.allowedIntervals.map(m=>h("option",{key:m,value:m},INTERVAL_LABELS[m]||m+" min")))),h("div",{className:"rlcc-field"},h("label",null,"Toast notifications"),h("button",{className:"rlcc-switch "+(toastEnabled?"rlcc-switch--on":"rlcc-switch--off"),onClick:handleToastToggle,type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},toastEnabled?"On":"Off"))))),h("div",{className:"rlcc-card"},h("h3",{className:"rlcc-card-title"},"Cache Types",h(InfoTip,{text:"Whatever you switch ON here gets stored; the timer above is what clears it back out again."})),h("div",{className:"rlcc-grid rlcc-grid--4"},h("div",{className:"rlcc-field"},h("label",null,"Public Cache"),h("button",{className:"rlcc-switch "+(cachePublic?"rlcc-switch--on":"rlcc-switch--off"),onClick:()=>handleCacheToggle("cache_public"),type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},cachePublic?"On":"Off"))),h("div",{className:"rlcc-field"},h("label",null,"Private Cache"),h("button",{className:"rlcc-switch "+(cachePrivate?"rlcc-switch--on":"rlcc-switch--off"),onClick:()=>handleCacheToggle("cache_private"),type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},cachePrivate?"On":"Off"))),h("div",{className:"rlcc-field"},h("label",null,"Object Cache"),h("button",{className:"rlcc-switch "+(cacheObject?"rlcc-switch--on":"rlcc-switch--off"),onClick:()=>handleCacheToggle("cache_object"),type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},cacheObject?"On":"Off"))),h("div",{className:"rlcc-field"},h("label",null,"Browser Cache"),h("button",{className:"rlcc-switch "+(cacheBrowser?"rlcc-switch--on":"rlcc-switch--off"),onClick:()=>handleCacheToggle("cache_browser"),type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},cacheBrowser?"On":"Off"))))),h("div",{className:"rlcc-card rlcc-card--wide"},h("h3",{className:"rlcc-card-title"},"Last Clear Report"),h(ClearReportCard)),h("div",{className:"rlcc-card"},h("h3",{className:"rlcc-card-title"},"Persistent Object Cache",h(InfoTip,{text:"A persistent object cache lets WordPress reuse database query results across requests. Off by default. Turning it on automatically detects and uses the fastest backend this server actually has working — Redis, then Memcached, then APCu, then a universal file-based fallback — with no manual picking or testing required."})),ocForeignDropin&&h("p",{className:"rlcc-oc-unavailable"},"A different object-cache.php already exists in wp-content/ — remove it first if you want this plugin to manage the persistent object cache."),h("div",{className:"rlcc-field"},h("label",null,"Enable ("+(ocDropinActive?"drop-in active":"not active")+")"),h("button",{className:"rlcc-switch "+(ocEnabled?"rlcc-switch--on":"rlcc-switch--off"),onClick:handleOcToggle,type:"button",disabled:ocSaving||ocForeignDropin},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},ocEnabled?"On":"Off"))),ocEnabled&&h("div",{className:"rlcc-oc-auto-status"},h("span",null,"Backend in use: ",h("strong",null,OC_BACKEND_LABELS[ocBackend]||ocBackend)),h("button",{className:"rlcc-oc-redetect-btn",onClick:handleOcRedetect,type:"button",disabled:ocSaving},ocSaving?"Checking…":"Re-detect best backend")),ocEnabled&&"redis"===ocBackend&&h("div",{className:"rlcc-oc-backend-row"},h("div",{className:"rlcc-field"},h("label",null,"Redis Host"),h("input",{type:"text",className:"rlcc-input",value:ocRedisHost,onChange:e=>{setOcRedisHost(e.target.value);ocRedisHostRef.current=e.target.value},onBlur:handleOcFieldCommit})),h("div",{className:"rlcc-field"},h("label",null,"Redis Port"),h("input",{type:"number",className:"rlcc-input",value:ocRedisPort,onChange:e=>{setOcRedisPort(e.target.value);ocRedisPortRef.current=e.target.value},onBlur:handleOcFieldCommit}))),ocEnabled&&"memcached"===ocBackend&&h("div",{className:"rlcc-oc-backend-row"},h("div",{className:"rlcc-field"},h("label",null,"Memcache Host"),h("input",{type:"text",className:"rlcc-input",value:ocMemcachedHost,onChange:e=>{setOcMemcachedHost(e.target.value);ocMemcachedHostRef.current=e.target.value},onBlur:handleOcFieldCommit})),h("div",{className:"rlcc-field"},h("label",null,"Memcache Port"),h("input",{type:"number",className:"rlcc-input",value:ocMemcachedPort,onChange:e=>{setOcMemcachedPort(e.target.value);ocMemcachedPortRef.current=e.target.value},onBlur:handleOcFieldCommit}))),"idle"!==ocTestStatus&&h("div",{className:"rlcc-oc-status rlcc-oc-status--"+("testing"===ocTestStatus?"idle":ocTestStatus)},"testing"===ocTestStatus?"Checking for the fastest available backend…":ocTestMessage)),h("div",{className:"rlcc-card"},h("h3",{className:"rlcc-card-title"},"Early-Bypass Page Cache",h(InfoTip,{text:"Serves cached pages before WordPress even loads. Only anonymous, cookie-free visitors; everyone else gets Private Cache instead. This is what actually improves server response time (TTFB) on speed tests."})),ecForeignDropin&&h("p",{className:"rlcc-oc-unavailable"},"A different advanced-cache.php already exists in wp-content/ — remove it first if you want this plugin to manage early-bypass caching."),!cachePublic&&!ecEnabled&&h("p",{className:"rlcc-oc-unavailable"},"Turn on Public Cache first — early-bypass caching serves the same cached files, just before WordPress loads."),h("div",{className:"rlcc-field"},h("label",null,"Enable ("+(ecDropinActive?"drop-in active":"not active")+(ecEnabled?", WP_CACHE "+(ecWpCacheTrue?"on":"NOT set"):"")+")"),h("button",{className:"rlcc-switch "+(ecEnabled?"rlcc-switch--on":"rlcc-switch--off"),onClick:handleEarlyCacheToggle,type:"button",disabled:ecSaving||ecForeignDropin||!cachePublic&&!ecEnabled},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},ecEnabled?"On":"Off"))),"idle"!==ecResultStatus&&h("div",{className:"rlcc-oc-status rlcc-oc-status--"+ecResultStatus},ecResultMessage),!ecWpConfigWritable&&h("p",{className:"rlcc-oc-unavailable"},"wp-config.php doesn't look writable — if enabling doesn't take effect, add define('WP_CACHE', true); yourself near the top, right after the opening <?php tag.")),h("div",{className:"rlcc-card rlcc-card--wide"},h("h3",{className:"rlcc-card-title"},"Database Optimization",h(InfoTip,{text:"Cleans up post revisions, trashed items, spam comments, and expired transients, and defragments your database tables. Off by default — nothing runs until you click the button below, and only for the items you've checked."})),h("div",{className:"rlcc-db-grid"},[["revisions","Post Revisions"],["trashed_posts","Trashed Posts"],["spam_comments","Spam Comments"],["trashed_comments","Trashed Comments"],["expired_transients","Expired Transients"]].map(([key,label])=>h("label",{key:key,className:"rlcc-db-checkbox"},h("input",{type:"checkbox",checked:!!dbOptions[key],onChange:()=>toggleDbOption(key)}),h("span",null,label),h("span",{className:"rlcc-db-count"},dbCounts[key]||0))),h("label",{className:"rlcc-db-checkbox"},h("input",{type:"checkbox",checked:!!dbOptions.optimize_tables,onChange:()=>toggleDbOption("optimize_tables")}),h("span",null,"Optimize Database Tables"))),h("button",{className:"rlcc-clear-btn",onClick:handleDbOptimize,disabled:dbRunning,type:"button"},dbRunning?"Optimizing…":"Optimize Database Now"),dbLastTimestamp>0&&h("p",{className:"rlcc-hint"},"Last run: ",timeAgo(dbLastTimestamp),dbLastReport?" — "+Object.values(dbLastReport).reduce((sum,n)=>sum+(typeof n==="number"?n:0),0)+" item(s) cleaned":"")),h("div",{className:"rlcc-card rlcc-card--wide rlcc-card--danger"},h("h3",{className:"rlcc-card-title rlcc-card-title--danger"},"Danger Zone"),h("div",{className:"rlcc-danger-row"},h("div",{className:"rlcc-danger-text"},h("label",null,"Delete all data on uninstall",h(InfoTip,{text:"Off by default. When ON, removing this plugin from Plugins → Installed Plugins deletes all of its settings and cached files, and runs one last full-site cache clear in the background."}))),h("button",{className:"rlcc-switch rlcc-switch--danger "+(deleteOnUninstall?"rlcc-switch--on":"rlcc-switch--off"),onClick:handleDeleteOnUninstallToggle,type:"button"},h("span",{className:"rlcc-switch-knob"}),h("span",{className:"rlcc-switch-label"},deleteOnUninstall?"On":"Off"))))),h(ToastList,{toasts:toasts,remove:removeToast}))}onReady(function(){const root=document.getElementById("rlcc-settings-root");if(!root)return;try{const tree=h(RLCCErrorBoundary,null,h(App));if(wp.element.createRoot){wp.element.createRoot(root).render(tree)}else{wp.element.render(tree,root)}}catch(err){renderFatal("dashboard failed to load",err&&(err.message||String(err))||"Unknown error.");console.error("CacheNova dashboard mount error:",err)}})})(); 1 (function() { 2 function onReady(fn) { 3 if ("loading" === document.readyState) { 4 document.addEventListener("DOMContentLoaded", fn); 5 } else { 6 fn(); 7 } 8 } 9 function renderFatal(title, detail) { 10 const root = document.getElementById("rlcc-settings-root"); 11 if (!root) return; 12 root.innerHTML = ""; 13 const wrap = document.createElement("div"); 14 wrap.className = "rlcc-fatal-error"; 15 wrap.style.cssText = 'max-width:820px;margin:20px 0;padding:16px 20px;border:1px solid #e4e6eb;border-left:4px solid #c0392b;border-radius:6px;background:#fdf3f2;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;'; 16 const h4 = document.createElement("p"); 17 h4.style.cssText = "margin:0 0 6px;font-weight:600;color:#c0392b;"; 18 h4.textContent = "CacheNova — " + title; 19 const p = document.createElement("p"); 20 p.style.cssText = "margin:0;color:#3c3c3c;white-space:pre-wrap;word-break:break-word;"; 21 p.textContent = detail; 22 wrap.appendChild(h4); 23 wrap.appendChild(p); 24 root.appendChild(wrap); 25 } 26 if (typeof wp === "undefined" || !wp.element || !wp.apiFetch) { 27 onReady(function() { 28 renderFatal("dashboard could not start", "A required WordPress script (wp-element or wp-api-fetch) did not load on this page, most likely because another plugin or theme script on this screen errored first and stopped the rest of the queue. Open your browser console (F12) on this page for the first red error, or try disabling other plugins one at a time to find the conflict."); 29 }); 30 return; 31 } 32 const {createElement: h, useState: useState, useEffect: useEffect, useRef: useRef, Component: Component} = wp.element; 33 const apiFetch = wp.apiFetch; 34 apiFetch.use(apiFetch.createNonceMiddleware(RLCC_DATA.nonce)); 35 apiFetch.use(apiFetch.createRootURLMiddleware(RLCC_DATA.restUrl)); 36 const NS = "/" + RLCC_DATA.namespace; 37 const INTERVAL_LABELS = { 38 10: "10 minutes", 39 20: "20 minutes", 40 30: "30 minutes", 41 60: "1 hour", 42 1440: "1 day", 43 10080: "7 days", 44 43200: "30 days (monthly)" 45 }; 46 const DB_SCHEDULE_LABELS = { 47 off: "Off (manual only)", 48 daily: "Every 1 day", 49 weekly: "Every 7 days", 50 monthly: "Every month" 51 }; 52 const OC_BACKEND_LABELS = { 53 redis: "Redis", 54 memcached: "Memcache", 55 apcu: "APCu", 56 file: "File-based (universal fallback)" 57 }; 58 function formatCountdown(seconds) { 59 if (seconds === null) return "--:--"; 60 if (seconds <= 0) return "0:00"; 61 const m = Math.floor(seconds / 60); 62 const s = seconds % 60; 63 return m + ":" + String(s).padStart(2, "0"); 64 } 65 function timeAgo(timestamp) { 66 if (!timestamp) return "never"; 67 const seconds = Math.floor(Date.now() / 1e3) - timestamp; 68 if (seconds < 60) return "just now"; 69 const mins = Math.round(seconds / 60); 70 if (mins < 60) return mins + " min ago"; 71 const hrs = Math.round(mins / 60); 72 if (hrs < 24) return hrs + " hr ago"; 73 const days = Math.round(hrs / 24); 74 return days + (days === 1 ? " day ago" : " days ago"); 75 } 76 function InfoTip({text: text}) { 77 return h("span", { 78 className: "rlcc-infotip", 79 tabIndex: 0 80 }, h("span", { 81 className: "rlcc-infotip-icon", 82 "aria-hidden": "true" 83 }, "i"), h("span", { 84 className: "rlcc-infotip-text", 85 role: "tooltip" 86 }, text), h("span", { 87 className: "screen-reader-text" 88 }, text)); 89 } 90 function formatDateTime(timestamp) { 91 if (!timestamp) return "—"; 92 try { 93 return new Date(timestamp * 1e3).toLocaleString(undefined, { 94 dateStyle: "medium", 95 timeStyle: "short" 96 }); 97 } catch (e) { 98 return new Date(timestamp * 1e3).toString(); 99 } 100 } 101 const MODAL_ICONS = { 102 danger: "!", 103 warning: "!", 104 info: "i" 105 }; 106 function ConfirmModal({modal: modal, onCancel: onCancel, onConfirm: onConfirm}) { 107 const confirmRef = useRef(null); 108 const cancelRef = useRef(null); 109 useEffect(() => { 110 if (!modal) return; 111 const previous = document.activeElement; 112 const target = "danger" === modal.tone ? cancelRef.current : confirmRef.current; 113 if (target) target.focus(); 114 function onKey(e) { 115 if (e.key === "Escape") { 116 e.preventDefault(); 117 onCancel(); 118 } else if (e.key === "Tab") { 119 const nodes = [ cancelRef.current, confirmRef.current ].filter(Boolean); 120 if (!nodes.length) return; 121 const first = nodes[0], last = nodes[nodes.length - 1]; 122 if (e.shiftKey && document.activeElement === first) { 123 e.preventDefault(); 124 last.focus(); 125 } else if (!e.shiftKey && document.activeElement === last) { 126 e.preventDefault(); 127 first.focus(); 128 } 129 } 130 } 131 document.addEventListener("keydown", onKey); 132 return () => { 133 document.removeEventListener("keydown", onKey); 134 if (previous && previous.focus) previous.focus(); 135 }; 136 }, [ modal ]); 137 if (!modal) return null; 138 const tone = modal.tone || "info"; 139 return h("div", { 140 className: "rlcc-modal-overlay", 141 onMouseDown: e => { 142 if (e.target === e.currentTarget) onCancel(); 143 } 144 }, h("div", { 145 className: "rlcc-modal rlcc-modal--" + tone, 146 role: "alertdialog", 147 "aria-modal": "true", 148 "aria-labelledby": "rlcc-modal-title", 149 "aria-describedby": "rlcc-modal-desc" 150 }, h("div", { 151 className: "rlcc-modal-icon", 152 "aria-hidden": "true" 153 }, MODAL_ICONS[tone] || "i"), h("h3", { 154 className: "rlcc-modal-title", 155 id: "rlcc-modal-title" 156 }, modal.title), h("p", { 157 className: "rlcc-modal-text", 158 id: "rlcc-modal-desc" 159 }, modal.message), modal.points && modal.points.length ? h("ul", { 160 className: "rlcc-modal-points" 161 }, modal.points.map((pt, i) => h("li", { 162 key: i 163 }, pt))) : null, h("div", { 164 className: "rlcc-modal-actions" 165 }, h("button", { 166 type: "button", 167 ref: cancelRef, 168 className: "rlcc-modal-btn rlcc-modal-btn--ghost", 169 onClick: onCancel 170 }, modal.cancelLabel || "Cancel"), h("button", { 171 type: "button", 172 ref: confirmRef, 173 className: "rlcc-modal-btn rlcc-modal-btn--" + tone, 174 onClick: onConfirm 175 }, modal.confirmLabel || "Continue")))); 176 } 177 class RLCCErrorBoundary extends Component { 178 constructor(props) { 179 super(props); 180 this.state = { 181 error: null 182 }; 183 } 184 static getDerivedStateFromError(error) { 185 return { 186 error: error 187 }; 188 } 189 render() { 190 if (this.state.error) { 191 return h("div", { 192 className: "rlcc-fatal-error", 193 style: { 194 maxWidth: "820px", 195 margin: "20px 0", 196 padding: "16px 20px", 197 border: "1px solid #e4e6eb", 198 borderLeft: "4px solid #c0392b", 199 borderRadius: "6px", 200 background: "#fdf3f2" 201 } 202 }, h("p", { 203 style: { 204 margin: "0 0 6px", 205 fontWeight: 600, 206 color: "#c0392b" 207 } 208 }, "CacheNova — the dashboard hit an error"), h("p", { 209 style: { 210 margin: 0, 211 color: "#3c3c3c", 212 whiteSpace: "pre-wrap", 213 wordBreak: "break-word" 214 } 215 }, String(this.state.error && this.state.error.message || this.state.error))); 216 } 217 return this.props.children; 218 } 219 } 220 function ToastList({toasts: toasts, remove: remove}) { 221 return h("div", { 222 className: "rlcc-toast-container" 223 }, toasts.map(t => h("div", { 224 key: t.id, 225 className: "rlcc-toast rlcc-toast--" + t.type, 226 onClick: () => remove(t.id) 227 }, h("span", { 228 className: "rlcc-toast-icon" 229 }, t.type === "success" ? "✓" : t.type === "error" ? "✕" : "ℹ"), h("span", null, t.message)))); 230 } 231 function App() { 232 const [loading, setLoading] = useState(true); 233 const [interval_, setInterval_] = useState(5); 234 const [toastEnabled, setToastEnabled] = useState(true); 235 const [cachePublic, setCachePublic] = useState(true); 236 const [cachePrivate, setCachePrivate] = useState(true); 237 const [cacheObject, setCacheObject] = useState(false); 238 const [cacheBrowser, setCacheBrowser] = useState(false); 239 const [deleteOnUninstall, setDeleteOnUninstall] = useState(false); 240 const [dbCounts, setDbCounts] = useState({ 241 revisions: 0, 242 trashed_posts: 0, 243 spam_comments: 0, 244 trashed_comments: 0, 245 expired_transients: 0 246 }); 247 const [dbOptions, setDbOptions] = useState({ 248 revisions: true, 249 trashed_posts: true, 250 spam_comments: true, 251 trashed_comments: true, 252 expired_transients: true, 253 optimize_tables: false 254 }); 255 const [modal, setModal] = useState(null); 256 function askConfirm(opts) { 257 setModal(opts); 258 } 259 function closeModal() { 260 setModal(null); 261 } 262 function acceptModal() { 263 const fn = modal && modal.onConfirm; 264 setModal(null); 265 if (fn) fn(); 266 } 267 const [activeTab, setActiveTab] = useState(window.location.hash === "#database" ? "database" : "cache"); 268 const [dbSchedule, setDbSchedule] = useState("off"); 269 const [dbNextRun, setDbNextRun] = useState(0); 270 const [dbLastType, setDbLastType] = useState("manual"); 271 const [dbScheduleSaving, setDbScheduleSaving] = useState(false); 272 const [dbRunning, setDbRunning] = useState(false); 273 const [dbLastReport, setDbLastReport] = useState(null); 274 const [dbLastTimestamp, setDbLastTimestamp] = useState(0); 275 const [ocEnabled, setOcEnabled] = useState(false); 276 const [ocBackend, setOcBackend] = useState("apcu"); 277 const [ocRedisHost, setOcRedisHost] = useState("127.0.0.1"); 278 const [ocRedisPort, setOcRedisPort] = useState(6379); 279 const [ocMemcachedHost, setOcMemcachedHost] = useState("127.0.0.1"); 280 const [ocMemcachedPort, setOcMemcachedPort] = useState(11211); 281 const [ocAvailable, setOcAvailable] = useState({ 282 apcu: false, 283 redis: false, 284 memcached: false 285 }); 286 const [ocDropinActive, setOcDropinActive] = useState(false); 287 const [ocForeignDropin, setOcForeignDropin] = useState(false); 288 const [ocTestStatus, setOcTestStatus] = useState("idle"); 289 const [ocTestMessage, setOcTestMessage] = useState(""); 290 const [ocSaving, setOcSaving] = useState(false); 291 const [ecEnabled, setEcEnabled] = useState(false); 292 const [ecDropinActive, setEcDropinActive] = useState(false); 293 const [ecForeignDropin, setEcForeignDropin] = useState(false); 294 const [ecWpCacheTrue, setEcWpCacheTrue] = useState(false); 295 const [localSslFixActive, setLocalSslFixActive] = useState(false); 296 const [diviActive, setDiviActive] = useState(false); 297 const [diviPerRequest, setDiviPerRequest] = useState(false); 298 const [diviPurgeSaving, setDiviPurgeSaving] = useState(false); 299 const [ecWpConfigWritable, setEcWpConfigWritable] = useState(true); 300 const [ecSaving, setEcSaving] = useState(false); 301 const [ecResultStatus, setEcResultStatus] = useState("idle"); 302 const [ecResultMessage, setEcResultMessage] = useState(""); 303 const [nextRun, setNextRun] = useState(0); 304 const [lastCleared, setLastCleared] = useState(0); 305 const [lastClearReport, setLastClearReport] = useState(null); 306 const [secondsLeft, setSecondsLeft] = useState(null); 307 const [saving, setSaving] = useState(false); 308 const [clearing, setClearing] = useState(false); 309 const [toasts, setToasts] = useState([]); 310 const toastIdRef = useRef(0); 311 const toastEnabledRef = useRef(toastEnabled); 312 toastEnabledRef.current = toastEnabled; 313 const lastNotifiedClearRef = useRef(0); 314 const reminderFiredForRef = useRef(null); 315 const intervalRef = useRef(interval_); 316 intervalRef.current = interval_; 317 const cachePublicRef = useRef(cachePublic); 318 cachePublicRef.current = cachePublic; 319 const cachePrivateRef = useRef(cachePrivate); 320 cachePrivateRef.current = cachePrivate; 321 const cacheObjectRef = useRef(cacheObject); 322 cacheObjectRef.current = cacheObject; 323 const cacheBrowserRef = useRef(cacheBrowser); 324 cacheBrowserRef.current = cacheBrowser; 325 const deleteOnUninstallRef = useRef(deleteOnUninstall); 326 deleteOnUninstallRef.current = deleteOnUninstall; 327 const ocBackendRef = useRef(ocBackend); 328 ocBackendRef.current = ocBackend; 329 const ocRedisHostRef = useRef(ocRedisHost); 330 ocRedisHostRef.current = ocRedisHost; 331 const ocRedisPortRef = useRef(ocRedisPort); 332 ocRedisPortRef.current = ocRedisPort; 333 const ocMemcachedHostRef = useRef(ocMemcachedHost); 334 ocMemcachedHostRef.current = ocMemcachedHost; 335 const ocMemcachedPortRef = useRef(ocMemcachedPort); 336 ocMemcachedPortRef.current = ocMemcachedPort; 337 function pushToast(message, type) { 338 if (!toastEnabledRef.current) return; 339 const id = ++toastIdRef.current; 340 setToasts([{ 341 id: id, 342 message: message, 343 type: type 344 } ]); 345 setTimeout(() => { 346 setToasts(prev => prev.filter(t => t.id !== id)); 347 }, 3500); 348 } 349 function removeToast(id) { 350 setToasts(prev => prev.filter(t => t.id !== id)); 351 } 352 function fetchSettings(showErrorToast, isBackgroundPoll) { 353 return apiFetch({ 354 path: NS + "/settings", 355 method: "GET" 356 }).then(res => { 357 setInterval_(res.interval_minutes); 358 setToastEnabled(!!res.toast_enabled); 359 setCachePublic(!!res.cache_public); 360 setCachePrivate(!!res.cache_private); 361 setCacheObject(!!res.cache_object); 362 setCacheBrowser(!!res.cache_browser); 363 setDeleteOnUninstall(!!res.delete_data_on_uninstall); 364 setOcEnabled(!!res.object_cache_enabled); 365 setOcBackend(res.object_cache_backend || "apcu"); 366 setOcRedisHost(res.object_cache_redis_host || "127.0.0.1"); 367 setOcRedisPort(res.object_cache_redis_port || 6379); 368 setOcMemcachedHost(res.object_cache_memcached_host || "127.0.0.1"); 369 setOcMemcachedPort(res.object_cache_memcached_port || 11211); 370 setOcAvailable(res.object_cache_available_backends || { 371 apcu: false, 372 redis: false, 373 memcached: false 374 }); 375 setOcDropinActive(!!res.object_cache_dropin_active); 376 setOcForeignDropin(!!res.object_cache_foreign_dropin); 377 setEcEnabled(!!res.early_cache_enabled); 378 setEcDropinActive(!!res.advanced_cache_dropin_active); 379 setEcForeignDropin(!!res.advanced_cache_foreign_dropin); 380 setEcWpCacheTrue(!!res.wp_cache_constant_true); 381 setEcWpConfigWritable(res.wp_config_writable !== false); 382 setLocalSslFixActive(!!res.local_ssl_fix_active); 383 setDiviActive(!!res.divi_active); 384 setDiviPerRequest(!!res.divi_per_request_purge); 385 setDbCounts(res.db_counts || { 386 revisions: 0, 387 trashed_posts: 0, 388 spam_comments: 0, 389 trashed_comments: 0, 390 expired_transients: 0 391 }); 392 setDbSchedule(res.db_schedule || "off"); 393 setDbNextRun(res.db_next_run_timestamp || 0); 394 if (res.last_db_optimize) { 395 setDbLastReport(res.last_db_optimize.report || null); 396 setDbLastTimestamp(res.last_db_optimize.timestamp || 0); 397 setDbLastType(res.last_db_optimize.type || "manual"); 398 } 399 setNextRun(res.next_run_timestamp); 400 if (isBackgroundPoll && res.last_cleared_timestamp && res.last_cleared_timestamp !== lastNotifiedClearRef.current) { 401 lastNotifiedClearRef.current = res.last_cleared_timestamp; 402 pushToast("Cache auto-cleared in the background.", "success"); 403 } else if (!isBackgroundPoll) { 404 lastNotifiedClearRef.current = res.last_cleared_timestamp; 405 } 406 setLastCleared(res.last_cleared_timestamp); 407 setLastClearReport(res.last_clear_report || null); 408 setLoading(false); 409 }).catch(err => { 410 setLoading(false); 411 if (showErrorToast) { 412 const detail = err && (err.message || err.code) || "unknown error"; 413 pushToast("Could not load settings: " + detail, "error"); 414 } 415 }); 416 } 417 useEffect(() => { 418 fetchSettings(true, false); 419 }, []); 420 useEffect(() => { 421 if (!nextRun) { 422 setSecondsLeft(null); 423 return; 424 } 425 if (reminderFiredForRef.current !== nextRun) { 426 reminderFiredForRef.current = null; 427 } 428 function tick() { 429 const remaining = nextRun - Math.floor(Date.now() / 1e3); 430 const clamped = Math.max(0, remaining); 431 setSecondsLeft(clamped); 432 const totalSeconds = intervalRef.current * 60; 433 const halfPoint = Math.floor(totalSeconds / 2); 434 if (clamped <= halfPoint && clamped > 0 && reminderFiredForRef.current !== nextRun && totalSeconds >= 120) { 435 reminderFiredForRef.current = nextRun; 436 const mins = Math.round(clamped / 60); 437 const label = mins >= 1 ? mins + " min" : clamped + " sec"; 438 pushToast(label + " remaining until next cache clear.", "info"); 439 } 440 } 441 tick(); 442 const id = window.setInterval(tick, 1e3); 443 return () => window.clearInterval(id); 444 }, [ nextRun ]); 445 useEffect(() => { 446 const id = window.setInterval(() => fetchSettings(false, true), 15e3); 447 return () => window.clearInterval(id); 448 }, []); 449 function saveSettings(overrides, successMessage) { 450 const newInterval = "interval_minutes" in overrides ? overrides.interval_minutes : intervalRef.current; 451 setSaving(true); 452 apiFetch({ 453 path: NS + "/settings", 454 method: "POST", 455 data: { 456 interval_minutes: newInterval, 457 toast_enabled: "toast_enabled" in overrides ? overrides.toast_enabled : toastEnabledRef.current, 458 cache_public: "cache_public" in overrides ? overrides.cache_public : cachePublicRef.current, 459 cache_private: "cache_private" in overrides ? overrides.cache_private : cachePrivateRef.current, 460 cache_object: "cache_object" in overrides ? overrides.cache_object : cacheObjectRef.current, 461 cache_browser: "cache_browser" in overrides ? overrides.cache_browser : cacheBrowserRef.current, 462 delete_data_on_uninstall: "delete_data_on_uninstall" in overrides ? overrides.delete_data_on_uninstall : deleteOnUninstallRef.current 463 } 464 }).then(res => { 465 setNextRun(res.next_run_timestamp); 466 setSaving(false); 467 pushToast(successMessage || "Settings saved.", "success"); 468 }).catch(err => { 469 setSaving(false); 470 const detail = err && (err.message || err.code) || "unknown error"; 471 pushToast("Failed to save settings: " + detail, "error"); 472 console.error("ACC save settings error:", err); 473 }); 474 } 475 function handleIntervalChange(e) { 476 const val = parseInt(e.target.value, 10); 477 setInterval_(val); 478 saveSettings({ 479 interval_minutes: val 480 }, "Settings saved — auto-clear every " + (INTERVAL_LABELS[val] || val + " min") + "."); 481 } 482 function handleToastToggle() { 483 const newVal = !toastEnabled; 484 setToastEnabled(newVal); 485 toastEnabledRef.current = newVal; 486 saveSettings({ 487 toast_enabled: newVal 488 }); 489 } 490 function handleCacheToggle(key) { 491 const setters = { 492 cache_public: [ cachePublic, setCachePublic, cachePublicRef, "Public Cache" ], 493 cache_private: [ cachePrivate, setCachePrivate, cachePrivateRef, "Private Cache" ], 494 cache_object: [ cacheObject, setCacheObject, cacheObjectRef, "Object Cache" ], 495 cache_browser: [ cacheBrowser, setCacheBrowser, cacheBrowserRef, "Browser Cache" ] 496 }; 497 const [current, setState, ref, label] = setters[key]; 498 const newVal = !current; 499 setState(newVal); 500 ref.current = newVal; 501 saveSettings({ 502 [key]: newVal 503 }, label + " turned " + (newVal ? "ON" : "OFF") + "."); 504 } 505 function handleDeleteOnUninstallToggle() { 506 const newVal = !deleteOnUninstall; 507 const apply = () => { 508 setDeleteOnUninstall(newVal); 509 deleteOnUninstallRef.current = newVal; 510 saveSettings({ 511 delete_data_on_uninstall: newVal 512 }, "Delete all data on uninstall turned " + (newVal ? "ON" : "OFF") + "."); 513 }; 514 if (!newVal) { 515 apply(); 516 return; 517 } 518 askConfirm({ 519 tone: "danger", 520 title: "Delete all data on uninstall?", 521 message: "When you delete CacheNova from Plugins → Installed Plugins, everything it created will be removed. This cannot be undone.", 522 points: [ "All CacheNova settings and reports", "All cached files", "One final full-site cache clear" ], 523 confirmLabel: "Yes, turn it on", 524 onConfirm: apply 525 }); 526 } 527 function saveObjectCacheSettings(overrides, onDone) { 528 setOcSaving(true); 529 apiFetch({ 530 path: NS + "/settings", 531 method: "POST", 532 data: { 533 interval_minutes: intervalRef.current, 534 toast_enabled: toastEnabledRef.current, 535 cache_public: cachePublicRef.current, 536 cache_private: cachePrivateRef.current, 537 cache_object: cacheObjectRef.current, 538 cache_browser: cacheBrowserRef.current, 539 delete_data_on_uninstall: deleteOnUninstallRef.current, 540 object_cache_backend: ocBackendRef.current, 541 object_cache_redis_host: ocRedisHostRef.current, 542 object_cache_redis_port: ocRedisPortRef.current, 543 object_cache_memcached_host: ocMemcachedHostRef.current, 544 object_cache_memcached_port: ocMemcachedPortRef.current, 545 ...overrides 546 } 547 }).then(res => { 548 setOcSaving(false); 549 setOcEnabled(!!res.object_cache_enabled); 550 setOcBackend(res.object_cache_backend || ocBackendRef.current); 551 setOcDropinActive(!!res.object_cache_dropin_active); 552 const result = res.object_cache_result; 553 if (result) { 554 if (result.success) { 555 setOcTestStatus("ok"); 556 setOcTestMessage(result.message); 557 pushToast(result.message, "success"); 558 } else { 559 setOcTestStatus("error"); 560 setOcTestMessage(result.message); 561 pushToast(result.message, "error"); 562 } 563 } else { 564 pushToast("Object cache settings saved.", "success"); 565 } 566 if (onDone) onDone(result); 567 }).catch(err => { 568 setOcSaving(false); 569 const detail = err && (err.message || err.code) || "unknown error"; 570 pushToast("Failed to save object cache settings: " + detail, "error"); 571 }); 572 } 573 function handleOcToggle() { 574 const newVal = !ocEnabled; 575 if (newVal) { 576 askConfirm({ 577 tone: "info", 578 title: "Enable Persistent Object Cache?", 579 message: "CacheNova will install wp-content/object-cache.php, which changes how WordPress caches data across the whole site. It automatically picks the fastest backend your server has working.", 580 points: [ "Backend order: Redis, Memcached, APCu, then file-based", "No manual setup needed" ], 581 confirmLabel: "Enable", 582 onConfirm: () => { 583 setOcTestStatus("testing"); 584 saveObjectCacheSettings({ 585 object_cache_enabled: newVal, 586 object_cache_backend: "auto" 587 }); 588 } 589 }); 590 return; 591 } 592 saveObjectCacheSettings({ 593 object_cache_enabled: newVal 594 }); 595 } 596 function handleOcRedetect() { 597 setOcTestStatus("testing"); 598 saveObjectCacheSettings({ 599 object_cache_enabled: true, 600 object_cache_backend: "auto" 601 }); 602 } 603 function handleOcFieldCommit() { 604 setOcTestStatus("idle"); 605 if (ocEnabled) { 606 saveObjectCacheSettings({}); 607 } 608 } 609 function handleEarlyCacheToggle() { 610 const newVal = !ecEnabled; 611 if (newVal) { 612 if (!cachePublic) { 613 pushToast("Turn on Public Cache first — early-bypass caching serves the same cached files, just before WordPress loads.", "error"); 614 return; 615 } 616 askConfirm({ 617 tone: "info", 618 title: "Enable Early-Bypass Page Cache?", 619 message: "CacheNova will install wp-content/advanced-cache.php and add define('WP_CACHE', true) to wp-config.php, so cached pages are served before WordPress even loads.", 620 points: [ "Anonymous visitors only; logged-in users get Private Cache", "Removed automatically when you turn it off" ], 621 confirmLabel: "Enable", 622 onConfirm: () => runEarlyCache(newVal) 623 }); 624 return; 625 } 626 runEarlyCache(newVal); 627 } 628 function runEarlyCache(newVal) { 629 setEcSaving(true); 630 apiFetch({ 631 path: NS + "/settings", 632 method: "POST", 633 data: { 634 interval_minutes: intervalRef.current, 635 toast_enabled: toastEnabledRef.current, 636 cache_public: cachePublicRef.current, 637 cache_private: cachePrivateRef.current, 638 cache_object: cacheObjectRef.current, 639 cache_browser: cacheBrowserRef.current, 640 delete_data_on_uninstall: deleteOnUninstallRef.current, 641 early_cache_enabled: newVal 642 } 643 }).then(res => { 644 setEcSaving(false); 645 setEcEnabled(!!res.early_cache_enabled); 646 setEcDropinActive(!!res.advanced_cache_dropin_active); 647 setEcWpCacheTrue(!!res.wp_cache_constant_true); 648 setLocalSslFixActive(!!res.local_ssl_fix_active); 649 setDiviActive(!!res.divi_active); 650 setDiviPerRequest(!!res.divi_per_request_purge); 651 const result = res.early_cache_result; 652 if (result) { 653 setEcResultStatus(result.success ? "ok" : "error"); 654 setEcResultMessage(result.message); 655 pushToast(result.message, result.success ? "success" : "error"); 656 } else { 657 pushToast("Early-bypass cache settings saved.", "success"); 658 } 659 }).catch(err => { 660 setEcSaving(false); 661 const detail = err && (err.message || err.code) || "unknown error"; 662 pushToast("Failed to save early-bypass cache settings: " + detail, "error"); 663 }); 664 } 665 function toggleDiviPerRequest() { 666 if (diviPurgeSaving) return; 667 const enabled = !diviPerRequest; 668 if (enabled) { 669 askConfirm({ 670 tone: "warning", 671 title: "Enable Divi cache compatibility?", 672 message: "CacheNova will delete the Divi et-cache folder after every frontend request. This can noticeably slow your website, so use it only for troubleshooting.", 673 confirmLabel: "Enable anyway", 674 onConfirm: () => runDiviPerRequest(enabled) 675 }); 676 return; 677 } 678 runDiviPerRequest(enabled); 679 } 680 function runDiviPerRequest(enabled) { 681 setDiviPurgeSaving(true); 682 apiFetch({path: NS + "/divi-per-request", method: "POST", data: {enabled}}).then(res => { 683 setDiviPerRequest(!!res.enabled); 684 pushToast(res.enabled ? (res.folder_deleted ? "Divi et-cache folder deleted. Automatic deletion enabled." : "Divi setting enabled, but et-cache folder could not be deleted. Check filesystem permissions.") : "Divi Cache Compatibility disabled. No per-request folder deletion.", res.enabled && !res.folder_deleted ? "error" : "success"); 685 }).catch(err => pushToast("Could not save Divi setting: " + (err.message || "Unknown error"), "error")) 686 .finally(() => setDiviPurgeSaving(false)); 687 } 688 function dbReportTotal(report) { 689 if (!report) return 0; 690 return Object.keys(report).filter(k => k !== "optimize_tables").reduce((sum, k) => sum + (typeof report[k] === "number" ? report[k] : 0), 0); 691 } 692 function switchTab(tab) { 693 setActiveTab(tab); 694 try { 695 window.history.replaceState(null, "", tab === "database" ? "#database" : window.location.pathname + window.location.search); 696 } catch (e) {} 697 } 698 function handleDbScheduleChange(e) { 699 const val = e.target.value; 700 if (val !== "off") { 701 askConfirm({ 702 tone: "danger", 703 title: "Turn on automatic optimization?", 704 message: "CacheNova will run " + (DB_SCHEDULE_LABELS[val] || val).toLowerCase() + " in the background. Every run permanently deletes the items below. This cannot be undone.", 705 points: [ "All post revisions and trashed posts", "Spam and trashed comments", "Expired transients, then optimizes tables" ], 706 confirmLabel: "Turn on", 707 onConfirm: () => runDbSchedule(val) 708 }); 709 return; 710 } 711 runDbSchedule(val); 712 } 713 function runDbSchedule(val) { 714 const previous = dbSchedule; 715 setDbSchedule(val); 716 setDbScheduleSaving(true); 717 apiFetch({ 718 path: NS + "/settings", 719 method: "POST", 720 data: { 721 interval_minutes: intervalRef.current, 722 toast_enabled: toastEnabledRef.current, 723 db_schedule: val 724 } 725 }).then(res => { 726 setDbScheduleSaving(false); 727 setDbSchedule(res.db_schedule || val); 728 setDbNextRun(res.db_next_run_timestamp || 0); 729 pushToast(val === "off" ? "Automatic database optimization turned OFF." : "Automatic database optimization set: " + (DB_SCHEDULE_LABELS[val] || val) + ".", "success"); 730 }).catch(err => { 731 setDbScheduleSaving(false); 732 setDbSchedule(previous); 733 const detail = err && (err.message || err.code) || "unknown error"; 734 pushToast("Failed to save database schedule: " + detail, "error"); 735 }); 736 } 737 function toggleDbOption(key) { 738 setDbOptions(prev => ({ 739 ...prev, 740 [key]: !prev[key] 741 })); 742 } 743 function handleDbOptimize() { 744 const selected = Object.keys(dbOptions).filter(k => dbOptions[k]); 745 if (!selected.length) { 746 pushToast("Select at least one item to optimize.", "error"); 747 return; 748 } 749 askConfirm({ 750 tone: "danger", 751 title: "Optimize database now?", 752 message: "The selected items will be permanently deleted. Revisions, trash and spam cannot be recovered afterward.", 753 confirmLabel: "Optimize now", 754 onConfirm: runDbOptimize 755 }); 756 } 757 function runDbOptimize() { 758 setDbRunning(true); 759 apiFetch({ 760 path: NS + "/database/optimize", 761 method: "POST", 762 data: dbOptions 763 }).then(res => { 764 setDbRunning(false); 765 setDbLastReport(res.report); 766 setDbLastTimestamp(res.timestamp); 767 setDbLastType("manual"); 768 const total = dbReportTotal(res.report); 769 pushToast("Database optimized — " + total + " item(s) cleaned.", "success"); 770 fetchSettings(false, false); 771 }).catch(err => { 772 setDbRunning(false); 773 const detail = err && (err.message || err.code) || "unknown error"; 774 pushToast("Database optimization failed: " + detail, "error"); 775 console.error("ACC db optimize error:", err); 776 }); 777 } 778 function handleClearNow() { 779 setClearing(true); 780 apiFetch({ 781 path: NS + "/clear", 782 method: "POST" 783 }).then(res => { 784 setLastCleared(res.last_cleared_timestamp); 785 lastNotifiedClearRef.current = res.last_cleared_timestamp; 786 setNextRun(res.next_run_timestamp); 787 reminderFiredForRef.current = null; 788 setClearing(false); 789 pushToast(res.divi_folder_deleted === false ? (res.divi_folder_message || "Cache cleared, but Divi et-cache still exists.") : "All theme & plugin caches cleared. Timer restarted.", res.divi_folder_deleted === false ? "error" : "success"); 790 if (res.last_clear_report) { 791 setLastClearReport(res.last_clear_report); 792 } else { 793 setTimeout(() => fetchSettings(false, true), 1500); 794 } 795 }).catch(err => { 796 setClearing(false); 797 const detail = err && (err.message || err.code) || "unknown error"; 798 pushToast("Failed to clear cache: " + detail, "error"); 799 console.error("ACC clear cache error:", err); 800 }); 801 } 802 const REPORT_STATUS_META = { 803 ok: { 804 icon: "✓", 805 text: "Verified", 806 className: "rlcc-report-ok" 807 }, 808 triggered: { 809 icon: "↻", 810 text: "Triggered", 811 className: "rlcc-report-triggered" 812 }, 813 failed: { 814 icon: "✕", 815 text: "Failed", 816 className: "rlcc-report-failed" 817 } 818 }; 819 function ClearReportCard() { 820 if (!lastClearReport || !lastClearReport.items || !Object.keys(lastClearReport.items).length) { 821 return h("p", { 822 className: "rlcc-hint" 823 }, 'No cache clear has run yet since this page loaded — trigger one with "Clear Cache Now" or wait for the scheduled timer.'); 824 } 825 const items = lastClearReport.items; 826 const failedCount = Object.values(items).filter(i => i.status === "failed").length; 827 return h("div", null, h("p", { 828 className: "rlcc-hint" 829 }, ("full" === lastClearReport.type ? "Full clear" : "Scheduled (light) clear") + " — " + (failedCount ? failedCount + " layer(s) failed to verify." : "every checked layer verified successfully.")), h("ul", { 830 className: "rlcc-report-list" 831 }, Object.keys(items).map(key => { 832 const item = items[key]; 833 const meta = REPORT_STATUS_META[item.status] || REPORT_STATUS_META.triggered; 834 return h("li", { 835 key: key, 836 className: "rlcc-report-row " + meta.className 837 }, h("span", { 838 className: "rlcc-report-icon" 839 }, meta.icon), h("span", { 840 className: "rlcc-report-label" 841 }, item.label), h("span", { 842 className: "rlcc-report-badge" 843 }, meta.text)); 844 }))); 845 } 846 function renderDbTab() { 847 const scheduleOn = dbSchedule !== "off"; 848 return h("div", { 849 className: "rlcc-card-grid rlcc-card-grid--three" 850 }, h("div", { 851 className: "rlcc-card" 852 }, h("h3", { 853 className: "rlcc-card-title" 854 }, h("span", { 855 className: "rlcc-card-symbol", 856 "aria-hidden": "true" 857 }, "◷"), "Automatic Optimization", h(InfoTip, { 858 text: "Runs in the background on WordPress cron. Each run clears ALL post revisions, trashed posts, spam comments, trashed comments and expired transients, then optimizes the database tables. Off by default." 859 })), h("div", { 860 className: "rlcc-grid" 861 }, h("div", { 862 className: "rlcc-field" 863 }, h("label", null, "Auto-optimize every"), h("select", { 864 value: dbSchedule, 865 onChange: handleDbScheduleChange, 866 disabled: dbScheduleSaving, 867 className: "rlcc-select" 868 }, Object.keys(DB_SCHEDULE_LABELS).map(k => h("option", { 869 key: k, 870 value: k 871 }, DB_SCHEDULE_LABELS[k])))), h("div", { 872 className: "rlcc-field" 873 }, h("label", null, "Status"), h("div", { 874 className: "rlcc-db-status " + (scheduleOn ? "rlcc-db-status--on" : "rlcc-db-status--off") 875 }, scheduleOn ? "Active — next run " + formatDateTime(dbNextRun) : "Off — nothing runs automatically")))), h("div", { 876 className: "rlcc-card" 877 }, h("h3", { 878 className: "rlcc-card-title" 879 }, h("span", { 880 className: "rlcc-card-symbol", 881 "aria-hidden": "true" 882 }, "▤"), "Manual Optimization", h(InfoTip, { 883 text: "Cleans up post revisions, trashed items, spam comments, and expired transients, and defragments your database tables. Nothing runs until you click the button below, and only for the items you've checked." 884 })), h("div", { 885 className: "rlcc-db-grid" 886 }, [ [ "revisions", "Post Revisions" ], [ "trashed_posts", "Trashed Posts" ], [ "spam_comments", "Spam Comments" ], [ "trashed_comments", "Trashed Comments" ], [ "expired_transients", "Expired Transients" ] ].map(([key, label]) => h("label", { 887 key: key, 888 className: "rlcc-db-checkbox" 889 }, h("input", { 890 type: "checkbox", 891 checked: !!dbOptions[key], 892 onChange: () => toggleDbOption(key) 893 }), h("span", null, label), h("span", { 894 className: "rlcc-db-count" 895 }, dbCounts[key] || 0))), h("label", { 896 className: "rlcc-db-checkbox" 897 }, h("input", { 898 type: "checkbox", 899 checked: !!dbOptions.optimize_tables, 900 onChange: () => toggleDbOption("optimize_tables") 901 }), h("span", null, "Optimize Database Tables"))), h("button", { 902 className: "rlcc-clear-btn", 903 onClick: handleDbOptimize, 904 disabled: dbRunning, 905 type: "button" 906 }, dbRunning ? "Optimizing…" : "Optimize Database Now"), dbLastTimestamp > 0 && h("p", { 907 className: "rlcc-hint" 908 }, "Last run: ", timeAgo(dbLastTimestamp), " (" + (dbLastType === "auto" ? "automatic" : "manual") + ")", dbLastReport ? " — " + dbReportTotal(dbLastReport) + " item(s) cleaned" + (dbLastReport.optimize_tables ? ", " + dbLastReport.optimize_tables + " table(s) optimized" : "") : "")), h("div", { 909 className: "rlcc-card rlcc-card--danger" 910 }, h("h3", { 911 className: "rlcc-card-title rlcc-card-title--danger" 912 }, "Danger Zone"), h("div", { 913 className: "rlcc-danger-row" 914 }, h("div", { 915 className: "rlcc-danger-text" 916 }, h("label", null, "Delete all data on uninstall", h(InfoTip, { 917 text: "Off by default. When ON, removing this plugin from Plugins → Installed Plugins deletes all of its settings and cached files, and runs one last full-site cache clear in the background." 918 }))), h("button", { 919 className: "rlcc-switch rlcc-switch--danger " + (deleteOnUninstall ? "rlcc-switch--on" : "rlcc-switch--off"), 920 onClick: handleDeleteOnUninstallToggle, 921 type: "button" 922 }, h("span", { 923 className: "rlcc-switch-knob" 924 }), h("span", { 925 className: "rlcc-switch-label" 926 }, deleteOnUninstall ? "On" : "Off"))))); 927 } 928 if (loading) { 929 return h("div", { 930 className: "rlcc-dashboard-loading" 931 }, "Loading settings…"); 932 } 933 return h("div", { 934 className: "rlcc-dashboard" 935 }, h("div", { 936 className: "rlcc-hero" 937 }, h("div", { 938 className: "rlcc-hero-top" 939 }, h("div", { 940 className: "rlcc-hero-title-block" 941 }, h("h2", { 942 className: "rlcc-hero-title" 943 }, h("span", {className:"rlcc-brand-mark", "aria-hidden":"true"}, "✦"), "CacheNova", h("span", {className:"rlcc-version"}, "v1.1.0")), h("div", { 944 className: "rlcc-status-pill" 945 }, h("span", { 946 className: "rlcc-status-dot" 947 }), h("span", null, "Running in background"))), h("button", { 948 className: "rlcc-clear-btn", 949 onClick: handleClearNow, 950 disabled: clearing 951 }, clearing ? "Clearing…" : "Clear Cache Now")), h("div", { 952 className: "rlcc-hero-meta" 953 }, h("div", { 954 className: "rlcc-countdown-row" 955 }, h("span", null, "Next automatic clear in"), h("span", { 956 className: "rlcc-countdown" 957 }, formatCountdown(secondsLeft))), h("div", { 958 className: "rlcc-hero-meta-item" 959 }, "Last cleared: ", h("strong", null, timeAgo(lastCleared))), diviActive ? h("div", { 960 className: "rlcc-hero-meta-item rlcc-hero-meta-item--divi" 961 }, "Divi detected — cache cleared on updates and scheduled runs") : h("div", { 962 className: "rlcc-hero-meta-item" 963 }, "Works with WordPress themes — automatic cache clearing enabled"))), h("div", { 964 className: "rlcc-tabs", 965 role: "tablist" 966 }, [ [ "cache", "Cache Settings" ], [ "database", "Database Optimization" ] ].map(([key, label]) => h("button", { 967 key: key, 968 type: "button", 969 role: "tab", 970 "aria-selected": activeTab === key, 971 className: "rlcc-tab" + (activeTab === key ? " rlcc-tab--active" : ""), 972 onClick: () => switchTab(key) 973 }, label))), diviActive && activeTab === "cache" && h("div", {className:"rlcc-card rlcc-divi-compat"}, 974 h("h3", {className:"rlcc-card-title"}, "Divi Cache Compatibility"), 975 h("p", null, "Delete the entire wp-content/et-cache folder immediately when enabled and after frontend requests. Divi may recreate it. Can significantly slow your site."), 976 h("button", {type:"button", className:"rlcc-switch " + (diviPerRequest ? "rlcc-switch--on" : "rlcc-switch--off"), onClick:toggleDiviPerRequest, disabled:diviPurgeSaving, "aria-pressed":diviPerRequest}, 977 h("span", {className:"rlcc-switch-knob"}), h("span", {className:"rlcc-switch-label"}, diviPerRequest ? "On" : "Off")) 978 ), activeTab === "database" ? renderDbTab() : h("div", { 979 className: "rlcc-card-grid" 980 }, h("div", { 981 className: "rlcc-card" 982 }, h("h3", { 983 className: "rlcc-card-title" 984 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "◷"), "Schedule & Notifications", h(InfoTip, { 985 text: "Cache also clears instantly every time you save a page or post — Divi Builder, Gutenberg, or any other editor — no manual reload needed." 986 })), h("div", { 987 className: "rlcc-grid" 988 }, h("div", { 989 className: "rlcc-field" 990 }, h("label", null, "Auto-clear every"), h("select", { 991 value: interval_, 992 onChange: handleIntervalChange, 993 disabled: saving, 994 className: "rlcc-select" 995 }, RLCC_DATA.allowedIntervals.map(m => h("option", { 996 key: m, 997 value: m 998 }, INTERVAL_LABELS[m] || m + " min")))), h("div", { 999 className: "rlcc-field" 1000 }, h("label", null, "Toast notifications"), h("button", { 1001 className: "rlcc-switch " + (toastEnabled ? "rlcc-switch--on" : "rlcc-switch--off"), 1002 onClick: handleToastToggle, 1003 type: "button" 1004 }, h("span", { 1005 className: "rlcc-switch-knob" 1006 }), h("span", { 1007 className: "rlcc-switch-label" 1008 }, toastEnabled ? "On" : "Off"))))), h("div", { 1009 className: "rlcc-card" 1010 }, h("h3", { 1011 className: "rlcc-card-title" 1012 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "▦"), "Cache Types", h(InfoTip, { 1013 text: "Whatever you switch ON here gets stored; the timer above is what clears it back out again." 1014 })), h("div", { 1015 className: "rlcc-grid rlcc-grid--4" 1016 }, h("div", { 1017 className: "rlcc-field" 1018 }, h("label", null, "Public Cache"), h("button", { 1019 className: "rlcc-switch " + (cachePublic ? "rlcc-switch--on" : "rlcc-switch--off"), 1020 onClick: () => handleCacheToggle("cache_public"), 1021 type: "button" 1022 }, h("span", { 1023 className: "rlcc-switch-knob" 1024 }), h("span", { 1025 className: "rlcc-switch-label" 1026 }, cachePublic ? "On" : "Off"))), h("div", { 1027 className: "rlcc-field" 1028 }, h("label", null, "Private Cache"), h("button", { 1029 className: "rlcc-switch " + (cachePrivate ? "rlcc-switch--on" : "rlcc-switch--off"), 1030 onClick: () => handleCacheToggle("cache_private"), 1031 type: "button" 1032 }, h("span", { 1033 className: "rlcc-switch-knob" 1034 }), h("span", { 1035 className: "rlcc-switch-label" 1036 }, cachePrivate ? "On" : "Off"))), h("div", { 1037 className: "rlcc-field" 1038 }, h("label", null, "Object Cache"), h("button", { 1039 className: "rlcc-switch " + (cacheObject ? "rlcc-switch--on" : "rlcc-switch--off"), 1040 onClick: () => handleCacheToggle("cache_object"), 1041 type: "button" 1042 }, h("span", { 1043 className: "rlcc-switch-knob" 1044 }), h("span", { 1045 className: "rlcc-switch-label" 1046 }, cacheObject ? "On" : "Off"))), h("div", { 1047 className: "rlcc-field" 1048 }, h("label", null, "Browser Cache"), h("button", { 1049 className: "rlcc-switch " + (cacheBrowser ? "rlcc-switch--on" : "rlcc-switch--off"), 1050 onClick: () => handleCacheToggle("cache_browser"), 1051 type: "button" 1052 }, h("span", { 1053 className: "rlcc-switch-knob" 1054 }), h("span", { 1055 className: "rlcc-switch-label" 1056 }, cacheBrowser ? "On" : "Off"))))), h("div", { 1057 className: "rlcc-card" 1058 }, h("h3", { 1059 className: "rlcc-card-title" 1060 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "◇"), "Persistent Object Cache", h(InfoTip, { 1061 text: "A persistent object cache lets WordPress reuse database query results across requests. Off by default. Turning it on automatically detects and uses the fastest backend this server actually has working — Redis, then Memcached, then APCu, then a universal file-based fallback — with no manual picking or testing required." 1062 })), ocForeignDropin && h("p", { 1063 className: "rlcc-oc-unavailable" 1064 }, "An existing object-cache.php is already installed (possibly by the host or an inactive plugin). CacheNova will not overwrite it. Identify its owner and back it up before switching cache providers."), h("div", { 1065 className: "rlcc-field" 1066 }, h("label", null, "Enable (" + (ocDropinActive ? "drop-in active" : "not active") + ")"), h("button", { 1067 className: "rlcc-switch " + (ocEnabled ? "rlcc-switch--on" : "rlcc-switch--off"), 1068 onClick: handleOcToggle, 1069 type: "button", 1070 disabled: ocSaving || ocForeignDropin 1071 }, h("span", { 1072 className: "rlcc-switch-knob" 1073 }), h("span", { 1074 className: "rlcc-switch-label" 1075 }, ocEnabled ? "On" : "Off"))), ocEnabled && h("div", { 1076 className: "rlcc-oc-auto-status" 1077 }, h("span", null, "Backend in use: ", h("strong", null, OC_BACKEND_LABELS[ocBackend] || ocBackend)), h("button", { 1078 className: "rlcc-oc-redetect-btn", 1079 onClick: handleOcRedetect, 1080 type: "button", 1081 disabled: ocSaving 1082 }, ocSaving ? "Checking…" : "Re-detect best backend")), ocEnabled && "redis" === ocBackend && h("div", { 1083 className: "rlcc-oc-backend-row" 1084 }, h("div", { 1085 className: "rlcc-field" 1086 }, h("label", null, "Redis Host"), h("input", { 1087 type: "text", 1088 className: "rlcc-input", 1089 value: ocRedisHost, 1090 onChange: e => { 1091 setOcRedisHost(e.target.value); 1092 ocRedisHostRef.current = e.target.value; 1093 }, 1094 onBlur: handleOcFieldCommit 1095 })), h("div", { 1096 className: "rlcc-field" 1097 }, h("label", null, "Redis Port"), h("input", { 1098 type: "number", 1099 className: "rlcc-input", 1100 value: ocRedisPort, 1101 onChange: e => { 1102 setOcRedisPort(e.target.value); 1103 ocRedisPortRef.current = e.target.value; 1104 }, 1105 onBlur: handleOcFieldCommit 1106 }))), ocEnabled && "memcached" === ocBackend && h("div", { 1107 className: "rlcc-oc-backend-row" 1108 }, h("div", { 1109 className: "rlcc-field" 1110 }, h("label", null, "Memcache Host"), h("input", { 1111 type: "text", 1112 className: "rlcc-input", 1113 value: ocMemcachedHost, 1114 onChange: e => { 1115 setOcMemcachedHost(e.target.value); 1116 ocMemcachedHostRef.current = e.target.value; 1117 }, 1118 onBlur: handleOcFieldCommit 1119 })), h("div", { 1120 className: "rlcc-field" 1121 }, h("label", null, "Memcache Port"), h("input", { 1122 type: "number", 1123 className: "rlcc-input", 1124 value: ocMemcachedPort, 1125 onChange: e => { 1126 setOcMemcachedPort(e.target.value); 1127 ocMemcachedPortRef.current = e.target.value; 1128 }, 1129 onBlur: handleOcFieldCommit 1130 }))), "idle" !== ocTestStatus && h("div", { 1131 className: "rlcc-oc-status rlcc-oc-status--" + ("testing" === ocTestStatus ? "idle" : ocTestStatus) 1132 }, "testing" === ocTestStatus ? "Checking for the fastest available backend…" : ocTestMessage)), h("div", { 1133 className: "rlcc-card" 1134 }, h("h3", { 1135 className: "rlcc-card-title" 1136 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "⚡"), "Early-Bypass Page Cache", h(InfoTip, { 1137 text: "Serves cached pages before WordPress even loads. Only anonymous, cookie-free visitors; everyone else gets Private Cache instead. This is what actually improves server response time (TTFB) on speed tests." 1138 })), ecForeignDropin && h("p", { 1139 className: "rlcc-oc-unavailable" 1140 }, "An existing advanced-cache.php is already installed (possibly by the host or an inactive plugin). CacheNova will not overwrite it. Identify its owner and back it up before switching cache providers."), !cachePublic && !ecEnabled && h("p", { 1141 className: "rlcc-oc-unavailable" 1142 }, "Turn on Public Cache first — early-bypass caching serves the same cached files, just before WordPress loads."), h("div", { 1143 className: "rlcc-field" 1144 }, h("label", null, "Enable (" + (ecDropinActive ? "drop-in active" : "not active") + (ecEnabled ? ", WP_CACHE " + (ecWpCacheTrue ? "on" : "NOT set") : "") + ")"), h("button", { 1145 className: "rlcc-switch " + (ecEnabled ? "rlcc-switch--on" : "rlcc-switch--off"), 1146 onClick: handleEarlyCacheToggle, 1147 type: "button", 1148 disabled: ecSaving || ecForeignDropin || !cachePublic && !ecEnabled 1149 }, h("span", { 1150 className: "rlcc-switch-knob" 1151 }), h("span", { 1152 className: "rlcc-switch-label" 1153 }, ecEnabled ? "On" : "Off"))), "idle" !== ecResultStatus && h("div", { 1154 className: "rlcc-oc-status rlcc-oc-status--" + ecResultStatus 1155 }, ecResultMessage), !ecWpConfigWritable && h("p", { 1156 className: "rlcc-oc-unavailable" 1157 }, "wp-config.php doesn't look writable — if enabling doesn't take effect, add define('WP_CACHE', true); yourself near the top, right after the opening <?php tag.")), h("div", { 1158 className: "rlcc-card rlcc-card--wide" 1159 }, h("h3", { 1160 className: "rlcc-card-title" 1161 }, h("span", {className:"rlcc-card-symbol", "aria-hidden":"true"}, "▤"), "Last Clear Report"), h(ClearReportCard))), h(ConfirmModal, { 1162 modal: modal, 1163 onCancel: closeModal, 1164 onConfirm: acceptModal 1165 }), h(ToastList, { 1166 toasts: toasts, 1167 remove: removeToast 1168 })); 1169 } 1170 onReady(function() { 1171 const root = document.getElementById("rlcc-settings-root"); 1172 if (!root) return; 1173 try { 1174 const tree = h(RLCCErrorBoundary, null, h(App)); 1175 if (wp.element.createRoot) { 1176 wp.element.createRoot(root).render(tree); 1177 } else { 1178 wp.element.render(tree, root); 1179 } 1180 } catch (err) { 1181 renderFatal("dashboard failed to load", err && (err.message || String(err)) || "Unknown error."); 1182 console.error("CacheNova dashboard mount error:", err); 1183 } 1184 }); 1185 })(); -
cachenova/trunk/cachenova.php
r3738364 r3777263 4 4 * Plugin Name: CacheNova 5 5 * Plugin URI: https://wordpress.org/plugins/cachenova/ 6 * Description: Auto matically clears and manages WordPress cache layers on a scheduled basis, helping keep the site fresh and optimized without impacting performance.7 * Version: 1. 0.06 * Description: Auto clear cache on a schedule or on save. Clear browser, page and object cache, plus database optimization. Works with Divi, Elementor, WooCommerce. 7 * Version: 1.1.0 8 8 * Requires at least: 7.0 9 9 * Requires PHP: 8.0 … … 18 18 if ( ! defined( 'ABSPATH' ) ) exit; 19 19 20 define( 'RLCC_PLUGIN_VERSION', '1. 0.0' );20 define( 'RLCC_PLUGIN_VERSION', '1.1.0' ); 21 21 22 22 define( 'RLCC_PLUGIN_FILE', __FILE__ ); … … 38 38 const DEFERRED_HOOK = 'rlcc_deferred_full_clear_event'; 39 39 40 const ALLOWED_INTERVALS = [ 1, 3, 5, 10, 20, 30, 60]; 40 const ALLOWED_INTERVALS = [ 10, 20, 30, 60, 1440, 10080, 43200 ]; 41 42 const DB_CRON_HOOK = 'rlcc_db_optimize_event'; 43 const DB_SCHEDULES = [ 44 'daily' => 86400, 45 'weekly' => 604800, 46 'monthly' => 2592000, 47 ]; 41 48 42 49 const PUBLIC_CACHE_DIR = 'public'; … … 86 93 87 94 add_action( self::CRON_HOOK, [ $this, 'light_clear' ] ); 95 add_action( self::DB_CRON_HOOK, [ $this, 'run_scheduled_db_optimize' ] ); 96 add_action( 'init', [ $this, 'sync_db_cron' ], 30 ); 88 97 89 98 add_action( 'save_post', [ $this, 'full_clear' ] ); 99 add_action( 'deleted_post', [ $this, 'full_clear' ] ); 100 add_action( 'trashed_post', [ $this, 'full_clear' ] ); 101 add_action( 'untrashed_post', [ $this, 'full_clear' ] ); 102 add_action( 'wp_update_nav_menu', [ $this, 'full_clear' ] ); 103 add_action( 'update_option_theme_mods_' . get_stylesheet(), [ $this, 'full_clear' ] ); 90 104 add_action( 'et_save_post', [ $this, 'full_clear' ] ); 91 105 add_action( 'customize_save_after', [ $this, 'full_clear' ] ); … … 103 117 104 118 add_action( 'send_headers', [ $this, 'maybe_send_browser_cache_headers' ] ); 119 add_filter( 'style_loader_src', [ $this, 'bust_browser_cache_src' ], 20 ); 120 add_filter( 'script_loader_src', [ $this, 'bust_browser_cache_src' ], 20 ); 105 121 106 122 add_action( 'admin_menu', [ $this->admin, 'add_settings_page' ] ); … … 111 127 add_action( 'init', [ $this->divi, 'disable_static_css' ], 20 ); 112 128 113 add_action( 'shutdown', [ $this->divi, 'purge_et_cache_dir' ] ); 129 // Opt-in only: expensive, and can reduce cache performance. 130 add_action( 'shutdown', [ $this, 'maybe_purge_divi_after_request' ], PHP_INT_MAX - 1 ); 114 131 } 115 132 add_action( 'init', [ $this, 'maybe_run_overdue_clear' ] ); … … 117 134 add_action( 'admin_post_rlcc_manual_clear', [ $this->admin, 'manual_clear_fallback' ] ); 118 135 add_action( 'admin_bar_menu', [ $this->admin, 'add_admin_bar_button' ], 100 ); 136 add_action( 'admin_enqueue_scripts', [ $this->admin, 'enqueue_admin_bar_assets' ] ); 137 add_action( 'wp_enqueue_scripts', [ $this->admin, 'enqueue_admin_bar_assets' ] ); 138 } 139 140 /** Optional compatibility mode; never run for admin, AJAX, REST, or cron requests. */ 141 public function maybe_purge_divi_after_request() { 142 $settings = $this->get_settings(); 143 if ( empty( $settings['divi_per_request_purge'] ) || ! $this->divi->is_active() || is_admin() || wp_doing_ajax() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { 144 return; 145 } 146 // A short lock (an option row, no file handles) stops concurrent requests repeatedly deleting the same folder. 147 $now = time(); 148 if ( ! add_option( 'rlcc_divi_purge_lock', $now, '', false ) ) { 149 if ( ( $now - (int) get_option( 'rlcc_divi_purge_lock', 0 ) ) < 30 ) { 150 return; 151 } 152 update_option( 'rlcc_divi_purge_lock', $now, false ); // Stale lock from a crashed request. 153 } 154 try { 155 $this->divi->purge_et_cache_dir(); 156 } finally { 157 delete_option( 'rlcc_divi_purge_lock' ); 158 } 119 159 } 120 160 … … 128 168 'interval_minutes' => 60, 129 169 'toast_enabled' => true, 170 'divi_per_request_purge' => false, 130 171 131 172 'cache_public' => true, … … 135 176 136 177 'delete_data_on_uninstall' => false, 178 179 'db_schedule' => 'off', 137 180 138 181 'object_cache_enabled' => false, … … 156 199 } 157 200 158 public function save_settings( $interval_minutes, $toast_enabled, $cache_public = null, $cache_private = null, $cache_object = null, $cache_browser = null, $delete_data_on_uninstall = null ) {201 public function save_settings( $interval_minutes, $toast_enabled, $cache_public = null, $cache_private = null, $cache_object = null, $cache_browser = null, $delete_data_on_uninstall = null, $db_schedule = null ) { 159 202 $current = $this->get_settings(); 160 203 161 204 $interval_minutes = in_array( (int) $interval_minutes, self::ALLOWED_INTERVALS, true ) 162 205 ? (int) $interval_minutes 163 : 5;206 : 60; 164 207 165 208 $settings = array_merge( $current, [ … … 173 216 ? (bool) $current['delete_data_on_uninstall'] 174 217 : (bool) $delete_data_on_uninstall, 218 'db_schedule' => null === $db_schedule ? $this->sanitize_db_schedule( $current['db_schedule'] ) : $this->sanitize_db_schedule( $db_schedule ), 175 219 ] ); 176 220 $this->update_settings_option( $settings ); 177 $this->reschedule_cron(); 221 222 // Only restart the cache-clear timer when the interval really changed (or nothing is scheduled). 223 if ( (int) $current['interval_minutes'] !== $interval_minutes || ! wp_next_scheduled( self::CRON_HOOK ) ) { 224 $this->reschedule_cron(); 225 } 226 if ( $current['db_schedule'] !== $settings['db_schedule'] ) { 227 $this->reschedule_db_cron(); 228 } 178 229 179 230 if ( $current['cache_public'] && ! $settings['cache_public'] ) { … … 198 249 wp_unschedule_event( $timestamp, self::CRON_HOOK ); 199 250 } 200 wp_schedule_event( time(), self::CRON_SCHEDULE_ID, self::CRON_HOOK ); 251 wp_schedule_event( time() + max( 60, (int) $this->get_settings()['interval_minutes'] * 60 ), self::CRON_SCHEDULE_ID, self::CRON_HOOK ); 252 } 253 254 public function sanitize_db_schedule( $value ) { 255 $value = sanitize_key( (string) $value ); 256 return isset( self::DB_SCHEDULES[ $value ] ) ? $value : 'off'; 257 } 258 259 /** (Re)create the automatic database optimization event for the saved schedule. */ 260 public function reschedule_db_cron() { 261 wp_clear_scheduled_hook( self::DB_CRON_HOOK ); 262 $schedule = $this->sanitize_db_schedule( $this->get_settings()['db_schedule'] ); 263 if ( 'off' === $schedule ) { 264 return; 265 } 266 wp_schedule_event( time() + self::DB_SCHEDULES[ $schedule ], 'rlcc_db_' . $schedule, self::DB_CRON_HOOK ); 267 } 268 269 /** Keep the cron event in step with the saved setting (covers upgrades, restored sites, cleared cron tables). */ 270 public function sync_db_cron() { 271 $schedule = $this->sanitize_db_schedule( $this->get_settings()['db_schedule'] ); 272 $next = wp_next_scheduled( self::DB_CRON_HOOK ); 273 if ( 'off' === $schedule ) { 274 if ( $next ) { 275 wp_clear_scheduled_hook( self::DB_CRON_HOOK ); 276 } 277 return; 278 } 279 if ( ! $next ) { 280 $this->reschedule_db_cron(); 281 } 282 } 283 284 /** Automatic run: clears ALL selectable data types and optimizes tables. */ 285 public function run_scheduled_db_optimize() { 286 if ( 'off' === $this->sanitize_db_schedule( $this->get_settings()['db_schedule'] ) ) { 287 return; 288 } 289 $this->admin->optimize_database( [ 290 'revisions' => true, 291 'trashed_posts' => true, 292 'spam_comments' => true, 293 'trashed_comments' => true, 294 'expired_transients' => true, 295 'optimize_tables' => true, 296 ], 'auto' ); 201 297 } 202 298 … … 230 326 'display' => sprintf( 'Every %d Minute(s) (CacheNova)', $minutes ), 231 327 ]; 328 $schedules['rlcc_db_daily'] = [ 'interval' => self::DB_SCHEDULES['daily'], 'display' => 'Every 1 day (CacheNova database)' ]; 329 $schedules['rlcc_db_weekly'] = [ 'interval' => self::DB_SCHEDULES['weekly'], 'display' => 'Every 7 days (CacheNova database)' ]; 330 $schedules['rlcc_db_monthly'] = [ 'interval' => self::DB_SCHEDULES['monthly'], 'display' => 'Every month (CacheNova database)' ]; 232 331 return $schedules; 233 332 } … … 295 394 update_option( 'rlcc_installed_version', RLCC_PLUGIN_VERSION ); 296 395 if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { 297 wp_schedule_event( time() , self::CRON_SCHEDULE_ID, self::CRON_HOOK );396 wp_schedule_event( time() + max( 60, (int) $this->get_settings()['interval_minutes'] * 60 ), self::CRON_SCHEDULE_ID, self::CRON_HOOK ); 298 397 } 299 398 $this->divi->disable_static_css(); 300 399 $this->divi->purge_et_cache_dir(); 400 $this->sync_db_cron(); 301 401 $this->ensure_cache_dir( self::PUBLIC_CACHE_DIR ); 302 402 $this->ensure_cache_dir( self::PRIVATE_CACHE_DIR ); … … 313 413 wp_unschedule_event( $deferred, self::DEFERRED_HOOK ); 314 414 } 415 wp_clear_scheduled_hook( self::DB_CRON_HOOK ); 315 416 316 417 $this->drop_in->remove_object_cache_dropin(); … … 336 437 $report['divi_core_cache'] = [ 'label' => 'Divi core cache', 'status' => 'triggered' ]; 337 438 } 338 if ( class_exists( '\ ET\Builder\Framework\Utility\CacheUtility' ) ) {439 if ( class_exists( '\\ET\\Builder\\Framework\\Utility\\CacheUtility' ) ) { 339 440 delete_transient( 'et_core_page_resource_fallback' ); 340 441 } 341 442 342 443 $et_cache_path = trailingslashit( WP_CONTENT_DIR ) . 'et-cache'; 343 if ( is_dir( $et_cache_path ) ) { 344 $this->divi->purge_et_cache_dir(); 345 $report['divi_et_cache'] = [ 'label' => 'Divi et-cache folder', 'status' => ! is_dir( $et_cache_path ) ? 'ok' : 'failed' ]; 346 } 444 if ( file_exists( $et_cache_path ) || is_link( $et_cache_path ) ) { 445 $removed = $this->divi->purge_et_cache_dir(); 446 clearstatcache( true, $et_cache_path ); 447 $report['divi_et_cache'] = [ 'label' => 'Divi et-cache folder', 'status' => ( $removed && ! file_exists( $et_cache_path ) ) ? 'ok' : 'failed' ]; 448 } 449 } 450 451 /** Clear known theme, page-builder and cache plugins. Every call is guarded, so missing plugins are simply skipped. */ 452 private function purge_third_party_caches( array &$report ) { 453 $run = function ( $key, $label, $callback ) use ( &$report ) { 454 try { 455 call_user_func( $callback ); 456 $report[ $key ] = [ 'label' => $label, 'status' => 'triggered' ]; 457 } catch ( \Throwable $e ) { 458 $report[ $key ] = [ 'label' => $label, 'status' => 'failed' ]; 459 } 460 }; 461 462 if ( function_exists( 'rocket_clean_domain' ) ) { 463 $run( 'wp_rocket', 'WP Rocket', 'rocket_clean_domain' ); 464 } 465 if ( function_exists( 'w3tc_flush_all' ) ) { 466 $run( 'w3_total_cache', 'W3 Total Cache', 'w3tc_flush_all' ); 467 } 468 if ( function_exists( 'wp_cache_clear_cache' ) ) { 469 $run( 'wp_super_cache', 'WP Super Cache', 'wp_cache_clear_cache' ); 470 } 471 if ( class_exists( 'LiteSpeed_Cache_API' ) || defined( 'LSCWP_V' ) ) { 472 $run( 'litespeed_cache', 'LiteSpeed Cache', function () { 473 do_action( 'litespeed_purge_all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 474 } ); 475 } 476 if ( function_exists( 'sg_cachepress_purge_cache' ) ) { 477 $run( 'sg_optimizer', 'SiteGround Optimizer', 'sg_cachepress_purge_cache' ); 478 } 479 if ( function_exists( 'autoptimize_flush_pagecache' ) ) { 480 $run( 'autoptimize', 'Autoptimize', 'autoptimize_flush_pagecache' ); 481 } 482 if ( class_exists( 'autoptimizeCache' ) && method_exists( 'autoptimizeCache', 'clearall' ) ) { 483 $run( 'autoptimize_files', 'Autoptimize files', [ 'autoptimizeCache', 'clearall' ] ); 484 } 485 if ( function_exists( 'wpfc_clear_all_cache' ) ) { 486 $run( 'wp_fastest_cache', 'WP Fastest Cache', function () { wpfc_clear_all_cache( true ); } ); 487 } 488 if ( class_exists( 'Breeze_PurgeCache' ) && method_exists( 'Breeze_PurgeCache', 'breeze_cache_flush' ) ) { 489 $run( 'breeze', 'Breeze', [ 'Breeze_PurgeCache', 'breeze_cache_flush' ] ); 490 } 491 if ( class_exists( 'WP_Optimize' ) && function_exists( 'WP_Optimize' ) && method_exists( WP_Optimize(), 'get_page_cache' ) ) { 492 $run( 'wp_optimize', 'WP-Optimize', function () { WP_Optimize()->get_page_cache()->purge(); } ); 493 } 494 if ( class_exists( 'Cache_Enabler' ) && method_exists( 'Cache_Enabler', 'clear_complete_cache' ) ) { 495 $run( 'cache_enabler', 'Cache Enabler', [ 'Cache_Enabler', 'clear_complete_cache' ] ); 496 } 497 if ( has_action( 'wphb_clear_page_cache' ) ) { 498 $run( 'hummingbird', 'Hummingbird', function () { do_action( 'wphb_clear_page_cache' ); } ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 499 } 500 if ( class_exists( '\\NitroPack\\SDK\\Api\\Cache' ) || has_action( 'nitropack_purge_all' ) ) { 501 $run( 'nitropack', 'NitroPack', function () { do_action( 'nitropack_purge_all' ); } ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 502 } 503 if ( function_exists( 'flush_pagely_cache' ) || class_exists( 'PagelyCachePurge' ) ) { 504 $run( 'pagely', 'Pagely', function () { do_action( 'pagely_purge_all' ); } ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 505 } 506 if ( class_exists( 'WpeCommon' ) ) { 507 $run( 'wp_engine', 'WP Engine', function () { 508 if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) { WpeCommon::purge_memcached(); } 509 if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) { WpeCommon::purge_varnish_cache(); } 510 } ); 511 } 512 if ( function_exists( 'kinsta_clear_cache' ) || has_action( 'kinsta_clear_cache_all' ) ) { 513 $run( 'kinsta', 'Kinsta Cache', function () { do_action( 'kinsta_clear_cache_all' ); } ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 514 } 515 if ( function_exists( 'pantheon_clear_edge_all' ) ) { 516 $run( 'pantheon', 'Pantheon Edge', 'pantheon_clear_edge_all' ); 517 } 518 if ( has_action( 'cloudflare_purge_everything' ) ) { 519 $run( 'cloudflare', 'Cloudflare', function () { do_action( 'cloudflare_purge_everything' ); } ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 520 } 521 522 // Page builders and themes that keep their own generated CSS/JS/data caches. 523 if ( class_exists( '\\Elementor\\Plugin' ) && isset( \Elementor\Plugin::$instance->files_manager ) ) { 524 $run( 'elementor', 'Elementor CSS cache', function () { \Elementor\Plugin::$instance->files_manager->clear_cache(); } ); 525 } 526 if ( class_exists( 'FLBuilderModel' ) && method_exists( 'FLBuilderModel', 'delete_asset_cache_for_all_posts' ) ) { 527 $run( 'beaver_builder', 'Beaver Builder cache', function () { FLBuilderModel::delete_asset_cache_for_all_posts(); } ); 528 } 529 if ( function_exists( 'oxygen_vsb_cache_universal_css' ) ) { 530 $run( 'oxygen', 'Oxygen cache', 'oxygen_vsb_cache_universal_css' ); 531 } 532 if ( class_exists( '\\Bricks\\Assets' ) && method_exists( '\\Bricks\\Assets', 'delete_assets_cache' ) ) { 533 $run( 'bricks', 'Bricks cache', function () { \Bricks\Assets::delete_assets_cache(); } ); 534 } 535 if ( function_exists( 'wc_delete_product_transients' ) ) { 536 $run( 'woocommerce', 'WooCommerce transients', function () { 537 wc_delete_product_transients(); 538 if ( function_exists( 'wc_delete_shop_order_transients' ) ) { wc_delete_shop_order_transients(); } 539 if ( class_exists( 'WC_Cache_Helper' ) ) { WC_Cache_Helper::get_transient_version( 'product', true ); } 540 } ); 541 } 542 543 // Lets any other theme or plugin attach its own cache cleanup. 544 do_action( 'rlcc_purge_third_party_caches' ); 347 545 } 348 546 … … 356 554 } 357 555 $settings = $this->get_settings(); 358 $grace = max( 60, (int) $settings['interval_minutes'] * 60 );556 $grace = max( 3600, (int) $settings['interval_minutes'] * 60 ); 359 557 if ( time() < $next + $grace ) { 360 558 return; … … 364 562 } 365 563 set_transient( 'rlcc_overdue_clear_lock', 1, 30 ); 366 $this->light_clear();367 $this->reschedule_cron();564 // Do not run expensive cache/database work during a visitor request. 565 // WP-Cron will process the scheduled clear asynchronously. 368 566 } 369 567 … … 418 616 $report['page_cache_private'] = [ 'label' => 'Private Cache files', 'status' => $this->purge_page_cache( self::PRIVATE_CACHE_DIR ) ? 'ok' : 'failed' ]; 419 617 420 if ( function_exists( 'rocket_clean_domain' ) ) { 421 rocket_clean_domain(); 422 $report['wp_rocket'] = [ 'label' => 'WP Rocket', 'status' => 'triggered' ]; 423 } 424 if ( function_exists( 'w3tc_flush_all' ) ) { 425 w3tc_flush_all(); 426 $report['w3_total_cache'] = [ 'label' => 'W3 Total Cache', 'status' => 'triggered' ]; 427 } 428 if ( function_exists( 'wp_cache_clear_cache' ) ) { 429 wp_cache_clear_cache(); 430 $report['wp_super_cache'] = [ 'label' => 'WP Super Cache', 'status' => 'triggered' ]; 431 } 432 if ( class_exists( 'LiteSpeed_Cache_API' ) ) { 433 do_action( 'litespeed_purge_all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 434 $report['litespeed_cache'] = [ 'label' => 'LiteSpeed Cache', 'status' => 'triggered' ]; 435 } 436 if ( function_exists( 'sg_cachepress_purge_cache' ) ) { 437 sg_cachepress_purge_cache(); 438 $report['sg_optimizer'] = [ 'label' => 'SiteGround Optimizer', 'status' => 'triggered' ]; 439 } 440 if ( function_exists( 'autoptimize_flush_pagecache' ) ) { 441 autoptimize_flush_pagecache(); 442 $report['autoptimize'] = [ 'label' => 'Autoptimize', 'status' => 'triggered' ]; 618 $this->purge_third_party_caches( $report ); 619 620 // Other cache integrations may regenerate Divi assets; remove the root last. 621 $divi_path = trailingslashit( WP_CONTENT_DIR ) . 'et-cache'; 622 $divi_deleted = $this->divi->purge_et_cache_dir(); 623 clearstatcache( true, $divi_path ); 624 if ( file_exists( $divi_path ) || is_link( $divi_path ) || $this->divi->is_active() ) { 625 $report['divi_et_cache'] = [ 626 'label' => 'Divi et-cache folder', 627 'status' => ( $divi_deleted && ! file_exists( $divi_path ) ) ? 'ok' : 'failed', 628 ]; 443 629 } 444 630 … … 449 635 450 636 do_action( 'rlcc_after_cache_clear' ); 637 638 // Integrations hooked above may recreate et-cache; purge again now and once more at the end of the request. 639 if ( $this->divi->is_active() ) { 640 $divi_deleted = $this->divi->purge_et_cache_dir(); 641 clearstatcache( true, $divi_path ); 642 $report['divi_et_cache'] = [ 643 'label' => 'Divi et-cache folder', 644 'status' => ( $divi_deleted && ! file_exists( $divi_path ) ) ? 'ok' : 'failed', 645 ]; 646 update_option( 'rlcc_last_clear_report', [ 'type' => 'full', 'items' => $report ] ); 647 $this->divi->schedule_shutdown_purge(); 648 } 451 649 } 452 650 … … 457 655 if ( $item === '.' || $item === '..' ) continue; 458 656 $path = $dir . '/' . $item; 459 if ( is_dir( $path ) ) {657 if ( is_dir( $path ) && ! is_link( $path ) ) { 460 658 $this->recursive_clear_directory( $path ); 461 659 $this->fs()->rmdir( $path ); … … 624 822 } 625 823 824 /** 825 * Browser cache clear: after every cache clear the stored asset version changes, and this adds it to the 826 * URLs of this site's own CSS/JS files so visitors' browsers download fresh copies. Opt-in; front end only. 827 */ 828 public function bust_browser_cache_src( $src ) { 829 if ( is_admin() || ! is_string( $src ) || '' === $src ) { 830 return $src; 831 } 832 $settings = $this->get_settings(); 833 if ( empty( $settings['cache_browser'] ) ) { 834 return $src; 835 } 836 $version = (int) get_option( 'rlcc_asset_version', 0 ); 837 if ( $version < 1 ) { 838 return $src; 839 } 840 $src_host = wp_parse_url( $src, PHP_URL_HOST ); 841 if ( $src_host && strtolower( $src_host ) !== strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ) ) { 842 return $src; // Only this site's own files; never third-party URLs. 843 } 844 return add_query_arg( 'rlcc_v', $version, $src ); 845 } 846 626 847 public function maybe_send_browser_cache_headers() { 627 848 if ( is_admin() || headers_sent() ) { … … 633 854 $settings = $this->get_settings(); 634 855 if ( empty( $settings['cache_browser'] ) ) { 856 return; 857 } 858 $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; 859 if ( ! in_array( $method, [ 'GET', 'HEAD' ], true ) ) { 860 return; 861 } 862 // Never let browsers or proxies store personal, dynamic or private pages. 863 if ( is_user_logged_in() || ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) ) { 864 return; 865 } 866 if ( is_404() || is_search() || is_preview() || is_customize_preview() || ( is_singular() && post_password_required() ) ) { 867 return; 868 } 869 if ( function_exists( 'is_cart' ) && ( is_cart() || is_checkout() || ( function_exists( 'is_account_page' ) && is_account_page() ) ) ) { 635 870 return; 636 871 } -
cachenova/trunk/includes/admin/admin.php
r3738364 r3777263 31 31 'cache_browser' => [ 'required' => false ], 32 32 'delete_data_on_uninstall' => [ 'required' => false ], 33 'db_schedule' => [ 'required' => false ], 33 34 'object_cache_enabled' => [ 'required' => false ], 34 35 'object_cache_backend' => [ 'required' => false ], … … 42 43 ] ); 43 44 45 register_rest_route( RLCC_Cache_Cleaner::REST_NAMESPACE, '/divi-per-request', [ 46 'methods' => 'POST', 47 'callback' => [ $this, 'rest_divi_per_request' ], 48 'permission_callback' => [ $this, 'rest_permissions' ], 49 ] ); 50 44 51 register_rest_route( RLCC_Cache_Cleaner::REST_NAMESPACE, '/object-cache/test', [ 45 52 'methods' => 'POST', … … 59 66 'permission_callback' => [ $this, 'rest_permissions' ], 60 67 ] ); 68 } 69 70 public function rest_divi_per_request( $request ) { 71 $enabled = rest_sanitize_boolean( $request->get_param( 'enabled' ) ); 72 $settings = $this->plugin->get_settings(); 73 $settings['divi_per_request_purge'] = $enabled; 74 $this->plugin->update_settings_option( $settings ); 75 $deleted = null; 76 if ( $enabled ) { 77 $deleted = $this->plugin->divi->purge_et_cache_dir(); 78 } 79 return rest_ensure_response( [ 'enabled' => $enabled, 'folder_deleted' => $deleted ] ); 61 80 } 62 81 … … 77 96 $settings['advanced_cache_dropin_active'] = $this->plugin->drop_in->is_our_advanced_cache_dropin_active(); 78 97 $settings['advanced_cache_foreign_dropin'] = $this->plugin->drop_in->is_foreign_advanced_cache_dropin_present(); 98 $settings['object_cache_dropin_details'] = $this->plugin->drop_in->describe_foreign_dropin( 'object-cache.php' ); 99 $settings['advanced_cache_dropin_details'] = $this->plugin->drop_in->describe_foreign_dropin( 'advanced-cache.php' ); 79 100 $settings['wp_cache_constant_true'] = $this->plugin->drop_in->is_wp_cache_constant_true(); 80 101 $settings['wp_config_writable'] = $this->plugin->drop_in->wp_config_writable(); 81 102 $settings['local_ssl_fix_active'] = has_filter( 'https_local_ssl_verify', '__return_false' ); 82 103 $settings['divi_active'] = $this->plugin->divi->is_active(); 104 $settings['divi_per_request_purge'] = ! empty( $settings['divi_per_request_purge'] ); 83 105 $settings['last_db_optimize'] = get_option( 'rlcc_last_db_optimize', null ); 84 106 $settings['db_counts'] = $this->get_database_counts(); 107 $settings['db_next_run_timestamp'] = wp_next_scheduled( RLCC_Cache_Cleaner::DB_CRON_HOOK ); 108 $settings['db_schedule'] = $this->plugin->sanitize_db_schedule( $settings['db_schedule'] ); 85 109 return rest_ensure_response( $settings ); 86 110 } … … 112 136 $delete_data_on_uninstall = null !== $request->get_param( 'delete_data_on_uninstall' ) ? (bool) $request->get_param( 'delete_data_on_uninstall' ) : null; 113 137 114 $saved = $this->plugin->save_settings( $interval_minutes, $toast_enabled, $cache_public, $cache_private, $cache_object, $cache_browser, $delete_data_on_uninstall ); 138 $db_schedule = null !== $request->get_param( 'db_schedule' ) ? sanitize_key( (string) $request->get_param( 'db_schedule' ) ) : null; 139 140 $saved = $this->plugin->save_settings( $interval_minutes, $toast_enabled, $cache_public, $cache_private, $cache_object, $cache_browser, $delete_data_on_uninstall, $db_schedule ); 141 $saved['db_next_run_timestamp'] = wp_next_scheduled( RLCC_Cache_Cleaner::DB_CRON_HOOK ); 115 142 116 143 $oc_enabled = $request->get_param( 'object_cache_enabled' ); … … 169 196 170 197 public function rest_clear_cache( $request ) { 198 // Complete the purge before responding, so the dashboard reports the actual result. 171 199 $this->plugin->reschedule_cron(); 172 $data = [ 200 $this->plugin->full_clear(); 201 $report = get_option( 'rlcc_last_clear_report', [] ); 202 $divi_result = isset( $report['items']['divi_et_cache']['status'] ) ? $report['items']['divi_et_cache']['status'] : 'not_present'; 203 return rest_ensure_response( [ 173 204 'cleared' => true, 174 'last_cleared_timestamp' => time(),205 'last_cleared_timestamp' => (int) get_option( 'rlcc_last_cleared', time() ), 175 206 'next_run_timestamp' => wp_next_scheduled( RLCC_Cache_Cleaner::CRON_HOOK ), 176 ]; 177 178 if ( function_exists( 'fastcgi_finish_request' ) ) { 179 add_filter( 'rest_pre_serve_request', function ( $served, $result, $req, $server ) use ( $data ) { 180 if ( $served ) { 181 return $served; 182 } 183 status_header( 200 ); 184 header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) ); 185 echo wp_json_encode( $data ); 186 fastcgi_finish_request(); 187 $this->plugin->full_clear(); 188 return true; 189 }, 10, 4 ); 190 return rest_ensure_response( $data ); 191 } 192 193 $this->plugin->full_clear(); 194 return rest_ensure_response( $data ); 195 } 196 197 public function optimize_database( $options ) { 207 'last_clear_report' => $report, 208 'divi_folder_deleted' => ! is_dir( trailingslashit( WP_CONTENT_DIR ) . 'et-cache' ), 209 'divi_folder_status' => $divi_result, 210 'divi_folder_exists' => is_dir( trailingslashit( WP_CONTENT_DIR ) . 'et-cache' ), 211 'divi_folder_message' => is_dir( trailingslashit( WP_CONTENT_DIR ) . 'et-cache' ) ? 'Divi et-cache exists after the purge. It may have been recreated, or the web server lacks deletion permissions.' : 'Divi et-cache directory is absent after clearing.', 212 ] ); 213 } 214 215 public function optimize_database( $options, $type = 'manual' ) { 198 216 global $wpdb; 199 217 $report = []; … … 235 253 236 254 if ( ! empty( $options['expired_transients'] ) ) { 237 $count = (int) $wpdb->query( "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE '\_transient\_timeout\_%' AND b.option_name = CONCAT('_transient_', SUBSTRING(a.option_name, 20)) AND a.option_value < UNIX_TIMESTAMP()" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 238 $report['expired_transients'] = max( 0, (int) ( $count / 2 ) ); 255 // Use WordPress APIs so both database and external object-cache transients are handled safely, in batches. 256 $deleted = 0; 257 for ( $batch = 0; $batch < 20; $batch++ ) { 258 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 259 $expired = $wpdb->get_col( $wpdb->prepare( 260 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_value < %d LIMIT 500", 261 $wpdb->esc_like( '_transient_timeout_' ) . '%', time() 262 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 263 if ( empty( $expired ) ) { 264 break; 265 } 266 $batch_deleted = 0; 267 foreach ( $expired as $name ) { 268 $key = substr( $name, strlen( '_transient_timeout_' ) ); 269 if ( delete_transient( $key ) ) { 270 $batch_deleted++; 271 } else { 272 // Orphaned timeout row: remove it directly so the loop always makes progress. 273 delete_option( $name ); 274 $batch_deleted++; 275 } 276 } 277 $deleted += $batch_deleted; 278 if ( count( $expired ) < 500 ) { 279 break; 280 } 281 } 282 $report['expired_transients'] = $deleted; 239 283 } 240 284 … … 258 302 259 303 wp_cache_delete( 'rlcc_db_counts', 'cachenova' ); 260 update_option( 'rlcc_last_db_optimize', [ 'timestamp' => time(), 'report' => $report ] );304 update_option( 'rlcc_last_db_optimize', [ 'timestamp' => time(), 'report' => $report, 'type' => 'auto' === $type ? 'auto' : 'manual' ] ); 261 305 return $report; 262 306 } … … 297 341 } 298 342 299 $css_file = 'assets/css/cn-admin. min.css';300 $js_file = 'assets/js/cn-admin. min.js';343 $css_file = 'assets/css/cn-admin.css'; 344 $js_file = 'assets/js/cn-admin.js'; 301 345 302 346 wp_enqueue_style( … … 311 355 plugins_url( $js_file, RLCC_PLUGIN_FILE ), 312 356 [ 'wp-element', 'wp-api-fetch', 'wp-i18n' ], 313 RLCC_PLUGIN_VERSION,357 filemtime( plugin_dir_path( RLCC_PLUGIN_FILE ) . $js_file ), 314 358 true 315 359 ); … … 341 385 } 342 386 387 /** 388 * Top-bar "Clear Cache Now": spinning icon + toast, without a page reload. 389 * The link keeps its normal href, so it still works as a plain fallback if JavaScript fails. 390 */ 391 public function enqueue_admin_bar_assets() { 392 if ( ! is_admin_bar_showing() || ! current_user_can( 'manage_options' ) ) { 393 return; 394 } 395 $settings = $this->plugin->get_settings(); 396 397 $css = '#wpadminbar #wp-admin-bar-rlcc-clear-cache.rlcc-is-clearing>.ab-item{opacity:.85;cursor:progress}' 398 . '.rlcc-bar-toast{position:fixed;top:48px;right:20px;z-index:999999;display:flex;align-items:center;gap:10px;max-width:min(380px,calc(100vw - 32px));padding:13px 18px;border-radius:12px;background:#fff;color:#18223b;font:600 13.5px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border:1px solid #e7eaf2;border-left:4px solid #635bdf;box-shadow:0 12px 35px rgba(20,27,55,.2);cursor:pointer;animation:rlccBarToastIn .22s ease}' 399 . '.rlcc-bar-toast--success{border-left-color:#1f9d63}.rlcc-bar-toast--error{border-left-color:#c0392b}' 400 . '.rlcc-bar-toast-icon{flex:none;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#fff;background:#635bdf}' 401 . '.rlcc-bar-toast--success .rlcc-bar-toast-icon{background:#1f9d63}.rlcc-bar-toast--error .rlcc-bar-toast-icon{background:#c0392b}' 402 . '.rlcc-bar-toast.rlcc-bar-toast--out{opacity:0;transform:translateY(-6px);transition:opacity .25s ease,transform .25s ease}' 403 . '@keyframes rlccBarToastIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}' 404 . '@media(max-width:782px){.rlcc-bar-toast{top:54px;right:12px}}' 405 . '@media (prefers-reduced-motion:reduce){.rlcc-bar-toast{animation:none}}'; 406 wp_register_style( 'cachenova-adminbar', false, [], RLCC_PLUGIN_VERSION ); 407 wp_enqueue_style( 'cachenova-adminbar' ); 408 wp_add_inline_style( 'cachenova-adminbar', $css ); 409 410 $config = [ 411 'url' => esc_url_raw( rest_url( RLCC_Cache_Cleaner::REST_NAMESPACE . '/clear' ) ), 412 'nonce' => wp_create_nonce( 'wp_rest' ), 413 'toast' => ! empty( $settings['toast_enabled'] ), 414 'msgOk' => __( 'All theme & plugin caches cleared.', 'cachenova' ), 415 'msgDone' => __( 'Cache cleared.', 'cachenova' ), 416 'msgFail' => __( 'Could not clear the cache. Please try again.', 'cachenova' ), 417 'msgDivi' => __( 'Cache cleared, but the Divi et-cache folder still exists.', 'cachenova' ), 418 'msgWorking' => __( 'Clearing cache…', 'cachenova' ), 419 ]; 420 $js = <<<'JS' 421 (function(){ 422 var C = window.RLCC_BAR; if (!C) return; 423 function toast(msg, type){ 424 if (!C.toast) return; 425 var old = document.querySelector('.rlcc-bar-toast'); if (old) old.remove(); 426 var t = document.createElement('div'); 427 t.className = 'rlcc-bar-toast rlcc-bar-toast--' + (type || 'info'); 428 t.setAttribute('role', 'status'); 429 var i = document.createElement('span'); i.className = 'rlcc-bar-toast-icon'; 430 i.textContent = type === 'success' ? '\u2713' : (type === 'error' ? '\u2715' : 'i'); 431 var m = document.createElement('span'); m.textContent = msg; 432 t.appendChild(i); t.appendChild(m); document.body.appendChild(t); 433 var close = function(){ t.classList.add('rlcc-bar-toast--out'); setTimeout(function(){ t.remove(); }, 260); }; 434 t.addEventListener('click', close); setTimeout(close, 3500); 435 } 436 function init(){ 437 var node = document.getElementById('wp-admin-bar-rlcc-clear-cache'); if (!node) return; 438 var link = node.querySelector('a'); if (!link) return; 439 var busy = false; 440 var spin = node.querySelector('animateTransform'); 441 function setSpin(sec){ if (spin) { spin.setAttribute('dur', sec + 's'); } } 442 if (spin && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { spin.parentNode.removeChild(spin); spin = null; } 443 // Fallback path (JS-less click that reloaded the page): confirm it with a toast, then tidy the URL. 444 if (/[?&]cleared=1/.test(window.location.search)) { 445 toast(C.msgOk, 'success'); 446 try { var u = new URL(window.location.href); u.searchParams.delete('cleared'); window.history.replaceState(null, '', u.toString()); } catch (e) {} 447 } 448 link.addEventListener('click', function(e){ 449 e.preventDefault(); 450 if (busy) return; 451 busy = true; setSpin(0.6); node.classList.add('rlcc-is-clearing'); toast(C.msgWorking, 'info'); 452 fetch(C.url, { method: 'POST', credentials: 'same-origin', headers: { 'X-WP-Nonce': C.nonce, 'Content-Type': 'application/json' }, body: '{}' }) 453 .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) 454 .then(function(res){ 455 if (res && res.divi_folder_deleted === false) { toast(C.msgDivi, 'error'); } 456 else { toast(C.msgOk, 'success'); } 457 }) 458 .catch(function(){ toast(C.msgFail, 'error'); }) 459 .then(function(){ busy = false; setSpin(4); node.classList.remove('rlcc-is-clearing'); }); 460 }); 461 } 462 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } 463 })(); 464 JS; 465 wp_register_script( 'cachenova-adminbar', false, [], RLCC_PLUGIN_VERSION, true ); 466 wp_enqueue_script( 'cachenova-adminbar' ); 467 wp_add_inline_script( 'cachenova-adminbar', 'window.RLCC_BAR=' . wp_json_encode( $config ) . ';', 'before' ); 468 wp_add_inline_script( 'cachenova-adminbar', $js ); 469 } 470 343 471 public function add_admin_bar_button( $wp_admin_bar ) { 344 472 if ( ! current_user_can( 'manage_options' ) ) return; 473 // Inline SVG (CacheNova sparkle mark) so the button is recognisable on every admin and front-end page, with no extra CSS file. 474 $icon = '<span class="ab-icon rlcc-ab-icon" aria-hidden="true" style="display:inline-block;width:22px;height:22px;margin:-4px 7px 0 0;padding:0;vertical-align:middle;float:none;background:#7c6cf5;border-radius:6px;line-height:0;">' 475 . '<svg class="rlcc-ab-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" style="display:block;">' 476 . '<g class="rlcc-ab-star"><path fill="#ffffff" d="M11 3.8l1.7 4.7 4.7 1.7-4.7 1.7L11 16.6l-1.7-4.7-4.7-1.7 4.7-1.7z"/><animateTransform attributeName="transform" type="rotate" from="0 11 10.2" to="360 11 10.2" dur="4s" repeatCount="indefinite"/></g>' 477 . '<circle cx="16.6" cy="5.4" r="1.2" fill="#ffd86b"/>' 478 . '</svg></span>'; 345 479 $wp_admin_bar->add_node( [ 346 480 'id' => 'rlcc-clear-cache', 347 'title' => 'Clear Cache Now',481 'title' => $icon . '<span class="ab-label">' . esc_html__( 'Clear Cache Now', 'cachenova' ) . '</span>', 348 482 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=rlcc_manual_clear' ), 'rlcc_manual_clear_action', 'rlcc_nonce' ), 483 'meta' => [ 'title' => esc_attr__( 'CacheNova — clear all caches', 'cachenova' ) ], 349 484 ] ); 350 485 } -
cachenova/trunk/includes/core/class-cache-drop-in-manager.php
r3737033 r3777263 10 10 public function __construct( $plugin ) { 11 11 $this->plugin = $plugin; 12 } 13 14 /** Report existing drop-ins without overwriting third-party code or exposing secrets. */ 15 public function describe_foreign_dropin( $filename ) { 16 if ( ! in_array( $filename, [ 'object-cache.php', 'advanced-cache.php' ], true ) ) return 'Unknown file'; 17 $path = trailingslashit( WP_CONTENT_DIR ) . $filename; 18 if ( ! is_file( $path ) ) return 'No drop-in found'; 19 $head = @file_get_contents( $path, false, null, 0, 2048 ); 20 if ( false === $head ) return 'Drop-in exists but cannot be read'; 21 if ( false !== strpos( $head, 'cachenova-config/' ) || false !== strpos( $head, 'RLCC_' ) ) return 'CacheNova drop-in'; 22 if ( false !== stripos( $head, 'LiteSpeed' ) ) return 'Existing LiteSpeed drop-in'; 23 if ( false !== stripos( $head, 'Redis' ) ) return 'Existing Redis drop-in'; 24 if ( false !== stripos( $head, 'W3 Total Cache' ) ) return 'Existing W3 Total Cache drop-in'; 25 if ( false !== stripos( $head, 'WP Rocket' ) ) return 'Existing WP Rocket drop-in'; 26 return 'Existing WordPress drop-in (owner not identified)'; 12 27 } 13 28 … … 55 70 } 56 71 $head = @file_get_contents( $path, false, null, 0, 4096 ); 57 return $head && false !== strpos( $head, RLCC_Cache_Cleaner::OBJECT_CACHE_DROPIN_SIGNATURE ); 72 // Signature for current installs; config-path fingerprint for files written by earlier builds (which had no signature). 73 return $head && ( false !== strpos( $head, RLCC_Cache_Cleaner::OBJECT_CACHE_DROPIN_SIGNATURE ) || false !== strpos( $head, 'cachenova-config/object-cache-config.json' ) ); 58 74 } 59 75 … … 247 263 } 248 264 $head = @file_get_contents( $path, false, null, 0, 4096 ); 249 return $head && false !== strpos( $head, RLCC_Cache_Cleaner::ADVANCED_CACHE_DROPIN_SIGNATURE);265 return $head && ( false !== strpos( $head, RLCC_Cache_Cleaner::ADVANCED_CACHE_DROPIN_SIGNATURE ) || false !== strpos( $head, 'cachenova-config/early-cache-config.json' ) ); 250 266 } 251 267 -
cachenova/trunk/integration/class-rlcc-divi-integration.php
r3737033 r3777263 40 40 } 41 41 42 /** Ask Divi itself to drop its generated resources first (best effort, never fatal). */ 43 private function ask_divi_to_clear() { 44 try { 45 if ( function_exists( 'et_core_clear_wp_cache' ) ) { 46 et_core_clear_wp_cache(); 47 } 48 if ( class_exists( 'ET_Core_PageResource' ) && method_exists( 'ET_Core_PageResource', 'remove_static_resources' ) ) { 49 ET_Core_PageResource::remove_static_resources( 'all', 'all', true ); 50 } 51 } catch ( \Throwable $e ) { 52 // Divi internals differ between versions; the filesystem purge below is the real work. 53 } 54 } 55 56 /** Safety check: only ever delete wp-content/et-cache, with Windows-safe path comparison. */ 57 private function is_safe_et_cache_path( $path ) { 58 $root = realpath( WP_CONTENT_DIR ); 59 $actual = realpath( $path ); 60 if ( ! $root || ! $actual ) return false; 61 $root = untrailingslashit( wp_normalize_path( $root ) ); 62 $actual = untrailingslashit( wp_normalize_path( $actual ) ); 63 $parent = dirname( $actual ); 64 if ( DIRECTORY_SEPARATOR === '\\' ) { 65 $root = strtolower( $root ); 66 $parent = strtolower( $parent ); 67 } 68 return $parent === $root && 'et-cache' === strtolower( basename( $actual ) ); 69 } 70 71 /** WP_Filesystem handle shared with the main plugin (direct method). */ 72 private function fs() { 73 return ( $this->plugin && method_exists( $this->plugin, 'fs' ) ) ? $this->plugin->fs() : null; 74 } 75 76 /** Delete one file or link; fixes read-only permissions (common on Windows/Laragon/XAMPP) and retries once. */ 77 private function remove_file( $path ) { 78 wp_delete_file( $path ); 79 clearstatcache( true, $path ); 80 if ( file_exists( $path ) || is_link( $path ) ) { 81 $fs = $this->fs(); 82 if ( $fs ) { 83 $fs->chmod( $path, 0666 ); 84 } 85 wp_delete_file( $path ); 86 clearstatcache( true, $path ); 87 } 88 return ! ( file_exists( $path ) || is_link( $path ) ); 89 } 90 91 /** Remove an EMPTY directory (non-recursive), fixing permissions and retrying once. */ 92 private function remove_empty_dir( $dir ) { 93 $fs = $this->fs(); 94 if ( ! $fs ) { 95 return false; 96 } 97 $fs->delete( $dir, false, 'd' ); 98 clearstatcache( true, $dir ); 99 if ( file_exists( $dir ) ) { 100 $fs->chmod( $dir, 0777 ); 101 $fs->delete( $dir, false, 'd' ); 102 clearstatcache( true, $dir ); 103 } 104 return ! file_exists( $dir ); 105 } 106 107 /** Remove a symbolic link / junction itself, never its target. */ 108 private function remove_link( $path ) { 109 if ( ! $this->remove_file( $path ) && is_link( $path ) ) { 110 $fs = $this->fs(); 111 if ( $fs ) { 112 $fs->delete( $path, false, 'd' ); // Windows junctions are removed like empty directories. 113 } 114 } 115 clearstatcache( true, $path ); 116 return ! ( file_exists( $path ) || is_link( $path ) ); 117 } 118 119 /** Recursively delete a tree; never follows links. */ 120 private function remove_tree( $dir ) { 121 $items = @scandir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 122 if ( false === $items ) { 123 $fs = $this->fs(); 124 if ( $fs ) { 125 $fs->chmod( $dir, 0755 ); 126 } 127 $items = @scandir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 128 if ( false === $items ) return false; 129 } 130 $ok = true; 131 foreach ( $items as $name ) { 132 if ( '.' === $name || '..' === $name ) continue; 133 $item = $dir . DIRECTORY_SEPARATOR . $name; 134 if ( is_link( $item ) ) { 135 if ( ! $this->remove_link( $item ) ) $ok = false; 136 } elseif ( is_dir( $item ) ) { 137 if ( ! $this->remove_tree( $item ) ) $ok = false; 138 } elseif ( ! $this->remove_file( $item ) ) { 139 $ok = false; 140 } 141 } 142 if ( ! $this->remove_empty_dir( $dir ) ) $ok = false; 143 return $ok; 144 } 145 146 /** Remove the entire Divi et-cache directory; never follows symbolic links. */ 42 147 public function purge_et_cache_dir() { 43 148 $path = trailingslashit( WP_CONTENT_DIR ) . 'et-cache'; 44 if ( is_dir( $path ) ) { 45 $this->plugin->recursive_clear_directory( $path ); 46 $this->plugin->fs()->rmdir( $path ); 149 clearstatcache( true, $path ); 150 if ( ! file_exists( $path ) && ! is_link( $path ) ) return true; 151 152 $this->ask_divi_to_clear(); 153 154 clearstatcache( true, $path ); 155 if ( ! file_exists( $path ) && ! is_link( $path ) ) return true; 156 157 // A symlinked et-cache: remove only the link itself, never the target. 158 if ( is_link( $path ) ) { 159 return $this->remove_link( $path ); 47 160 } 161 if ( ! is_dir( $path ) ) { 162 return $this->remove_file( $path ); 163 } 164 if ( ! $this->is_safe_et_cache_path( $path ) ) return false; 165 166 $this->remove_tree( $path ); 167 clearstatcache( true, $path ); 168 if ( file_exists( $path ) ) { 169 // One retry: Divi or the OS may have been holding a file open a moment ago. 170 usleep( 150000 ); 171 $this->remove_tree( $path ); 172 clearstatcache( true, $path ); 173 } 174 return ! file_exists( $path ); 48 175 } 176 177 /** Delete et-cache once more at the very end of the request, after Divi's own shutdown writers have run. */ 178 public function schedule_shutdown_purge() { 179 static $registered = false; 180 if ( $registered ) return; 181 $registered = true; 182 add_action( 'shutdown', [ $this, 'purge_et_cache_dir' ], PHP_INT_MAX ); 183 } 184 49 185 } 50 186 -
cachenova/trunk/languages/cachenova.pot
r3737033 r3777263 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: CacheNova 1. 0.0\n"3 "Project-Id-Version: CacheNova 1.1.0\n" 4 4 "Report-Msgid-Bugs-To: https://shaoun18.github.io/\n" 5 5 "MIME-Version: 1.0\n" 6 6 "Content-Type: text/plain; charset=UTF-8\n" 7 7 "Content-Transfer-Encoding: 8bit\n" 8 "POT-Creation-Date: 2026- 09-25T00:00:00+00:00\n"9 "PO-Revision-Date: 2026- 09-25T00:00:00+00:00\n"8 "POT-Creation-Date: 2026-10-23T00:00:00+00:00\n" 9 "PO-Revision-Date: 2026-10-23T00:00:00+00:00\n" 10 10 "X-Generator: manual\n" 11 11 "X-Domain: cachenova\n" -
cachenova/trunk/readme.txt
r3738344 r3777263 1 === CacheNova ===1 === CacheNova – Auto Clear Cache, Browser Cache & Database Optimizer === 2 2 Contributors: shaounchandrashill 3 Tags: c ache, performance, object cache, page cache, developer tools3 Tags: clear cache, auto clear cache, browser cache, cache, performance 4 4 Requires at least: 7.0 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.0 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Auto matically clears and manages WordPress cache layers on a scheduled basis, helping keep the site fresh and optimized without impacting performance.11 Auto clear cache on a schedule or on save. Clear browser, page and object cache, plus database optimization. Works with Divi, Elementor, WooCommerce. 12 12 13 13 == Description == 14 14 15 A general-purpose cache-clearing plugin for any WordPress site. It works identically regardless of which theme or page builder you use — Divi, Elementor, Gutenberg, a classic theme, all of it — because it hooks into WordPress's own standard save/activate/deactivate events, not anything builder-specific.15 **CacheNova is an auto cache clear plugin for WordPress.** It clears your site's cache automatically, on a schedule you choose and every time you change something, so visitors always see your latest edits. Need it right now? Click **Clear Cache Now** in the top admin bar, on any page, with no page reload. 16 16 17 **What it does, on any site:** 17 Works with any theme and any page builder: Divi, Elementor, Beaver Builder, Bricks, Oxygen, the block editor and classic themes. 18 18 19 * Instantly clears every relevant cache the moment you save a page or post — the block editor, the Customizer, Divi Builder, or any other editor — no manual reload required. 20 * Runs a lightweight background job on a custom schedule (1, 3, 5, 10, 20, 30, or 60 minutes — your choice) to keep object cache and expired transients tidy, without touching site performance. 21 * Also clears automatically whenever any plugin on the site is activated, deactivated, or deleted. 22 * Detects and flushes WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, SiteGround Optimizer, and Autoptimize automatically if they're active — no extra configuration. 23 * Optional built-in Public/Private page caching, with a true early-bypass mode that serves cached pages before WordPress even loads. 24 * Optional persistent object cache — APCu, Redis, Memcached, or a zero-dependency File-based fallback — auto-detected and auto-selected, no manual backend picking or connection testing required. 25 * Automatically refreshes its own page-cache and object-cache files whenever the plugin itself is updated, so bug fixes and speed improvements actually take effect instead of a stale copy silently running forever. 26 * Optional Database Optimization — clean post revisions, trashed posts, spam comments, and expired transients, plus a table-optimization pass. Off by default, only runs when explicitly requested. 27 * Ships with a modern React-based settings panel (built on WordPress's own bundled React — no separate build step, no bloat) with live toast notifications and a real-time countdown to the next scheduled clear. 19 = Clear cache automatically (auto cache clear) = 28 20 29 **Why "light" vs "full" clearing matters** 21 * **Auto clear cache on a schedule** — every 10, 20 or 30 minutes, every hour, every day, every 7 days or every 30 days. 22 * **Auto clear cache on save** — clears the moment you update a post or page, save in the Customizer, edit a menu, switch theme, or activate, deactivate, update or delete a plugin. 23 * **Clear Cache Now button** in the WordPress admin bar and inside the plugin dashboard, with a live toast message and a spinning icon while it works. 24 * **Smart light and full clears** — the frequent scheduled clear is lightweight; the full clear only runs when something really changed. 30 25 31 Earlier cache-clearing approaches run a full clear (OPcache reset, full cache-plugin flush, filesystem walks) on every timer tick — which can itself slow a site down if it happens too often. This plugin splits the work: 26 = Browser cache clear = 32 27 33 * **Light clear** (runs on your background schedule): only the cache types you've switched on. Cheap, frequent, invisible. 34 * **Full clear** (runs only when something actually changed — a save, manual click, theme switch, or plugin update): everything, including OPcache and third-party cache plugin flushes. 28 Turn on **Browser Cache** and every cache clear gives your site's own CSS and JavaScript files a fresh version number, so visitors' browsers download the newest files instead of showing an old copy. Cache-Control headers are sent only to logged-out visitors, and never on cart, checkout, account, search, 404, preview or password-protected pages. It is off by default. 35 29 36 **Automatic bonus support for Divi** 30 = Clears every cache layer = 37 31 38 If Divi happens to be active on the site, the plugin detects it automatically and adds continuous purging of Divi's `et-cache` directory — a common source of "I edited it but nothing changed" on Divi sites — plus disabling Divi 4's Static CSS File Generation setting so the folder is never created in the first place. On Divi 5 (which removed that setting entirely), the plugin purges the folder after every request instead. None of this runs, and none of it is needed, on a site that isn't using Divi — the rest of the plugin works exactly the same either way. 32 * Object cache and expired transients 33 * PHP OPcache 34 * Built-in public and private page cache, plus an optional early-bypass mode that serves cached pages before WordPress loads 35 * Optional persistent object cache: Redis, Memcached, APCu or a file-based fallback, picked automatically 36 * **Other cache plugins, when active:** WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, SiteGround Optimizer, Autoptimize, WP Fastest Cache, Breeze, WP-Optimize, Cache Enabler, Hummingbird and NitroPack 37 * **Page builders and WooCommerce, when active:** Elementor, Beaver Builder, Bricks, Oxygen and WooCommerce product transients 38 * **Hosting caches, when detected:** WP Engine, Kinsta, Pantheon, Pagely and Cloudflare plugin hooks 39 * Developers can hook their own cleanup into `rlcc_purge_third_party_caches` 40 41 = Database optimization (automatic or manual) = 42 43 A dedicated **Database Optimization** tab keeps your database small and fast: 44 45 * Auto-optimize **every 1 day, every 7 days or every month**. Each run clears all post revisions, trashed posts, spam comments, trashed comments and expired transients, then optimizes your database tables. 46 * Or run a manual cleanup whenever you want, choosing exactly which items to clean. 47 * Off by default. Nothing is deleted until you turn it on and confirm. 48 49 = Divi cache clear (automatic bonus support) = 50 51 If Divi is active, CacheNova clears Divi's `et-cache` folder every time you clear the cache, fixing the common "I edited it but nothing changed" problem. It handles read-only files and symlinks safely, and only ever removes the `et-cache` folder inside wp-content. On Divi 4 it also turns off Static CSS File Generation. Nothing Divi-specific runs on sites that don't use Divi. 52 53 = A modern, simple dashboard = 54 55 * Built on WordPress's own bundled React, with no extra bundles or build step 56 * Clear confirmation windows before anything risky, and live toast notifications 57 * Real-time countdown to the next automatic clear 58 * Last clear report that shows exactly what was cleared and verified 39 59 40 60 == Installation == 41 61 42 1. Upload the plugin folder to `/wp-content/plugins/`, or install the zip directly through Plugins → Add New → Upload Plugin. 43 2. Activate the plugin through the 'Plugins' menu in WordPress. 44 3. Go to Settings → CacheNova to choose your auto-clear interval and toggle toast notifications. 45 46 No further configuration is required — Divi's Static CSS File Generation is disabled automatically the moment the plugin activates. 62 1. Upload the plugin folder to `/wp-content/plugins/`, or go to Plugins → Add New → Upload Plugin and upload the zip. 63 2. Activate the plugin through the Plugins menu in WordPress. 64 3. Go to Settings → CacheNova to choose how often to auto clear the cache. Optional features such as Browser Cache and Database Optimization are off until you turn them on. 47 65 48 66 == Frequently Asked Questions == 49 67 50 = Does this only work with Divi, or with any theme and plugin? =68 = How do I clear the cache in WordPress? = 51 69 52 Any theme, any plugin. The core of this plugin — clearing transients, object cache, OPcache, its own optional page cache, and popular third-party cache plugins, on a schedule and on every save — hooks into WordPress's own standard events (`save_post`, `switch_theme`, `activated_plugin`, and so on), which fire identically no matter what theme or page builder is active. Divi support is a bonus that's detected and applied automatically only if Divi happens to be installed; nothing Divi-specific runs, and nothing is required, on a site that isn't using it.70 Click **Clear Cache Now** in the top admin bar, or open Settings → CacheNova and use the button at the top. It clears every cache layer in one click and shows a message when it is done. 53 71 54 = Will this conflict with my other caching plugin? =72 = How do I auto clear cache in WordPress? = 55 73 56 No. It detects common caching plugins (WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, SiteGround Optimizer, Autoptimize) and flushes them as part of a full clear, rather than working against them. 74 Open Settings → CacheNova and choose **Auto-clear every** (10, 20 or 30 minutes, 1 hour, 1 day, 7 days or 30 days). CacheNova also clears automatically whenever you save content, change a menu or theme, or activate, deactivate or update a plugin. 75 76 = How do I clear the browser cache for my WordPress site? = 77 78 Turn on **Browser Cache** under Settings → CacheNova → Cache Types. After that, every cache clear changes the version number on your site's own CSS and JavaScript files, so visitors' browsers fetch fresh copies. This works for all visitors without asking them to press Ctrl+F5. 79 80 = Does it work with Divi, Elementor, WooCommerce and other themes and plugins? = 81 82 Yes. CacheNova hooks into WordPress's own standard events, so it works with any theme and page builder. It also clears the caches of Divi, Elementor, Beaver Builder, Bricks, Oxygen and WooCommerce product transients when they are active. 83 84 = Will it conflict with my other caching plugin? = 85 86 No. It detects popular caching plugins (WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, SiteGround Optimizer, Autoptimize, WP Fastest Cache, Breeze, WP-Optimize, Cache Enabler, Hummingbird and NitroPack) and clears them as part of a full clear instead of working against them. 87 88 = Is Browser Cache safe for logged-in users and WooCommerce? = 89 90 Yes. Cache headers are never sent to logged-in users, on WooCommerce cart, checkout or account pages, on search, 404 or preview pages, on password-protected posts, or on non-GET requests. 91 92 = Is the automatic database optimization safe? = 93 94 It permanently deletes post revisions, trashed posts, spam comments, trashed comments and expired transients, so it is **off by default** and asks you to confirm before it turns on. Take a backup first if you want to keep your revisions. 95 96 = Does the background schedule slow my site down? = 97 98 No. The scheduled run is a lightweight clear. The heavier full clear only runs when something changes, or when you click Clear Cache Now. 99 100 = The Divi et-cache folder was not being removed. Is that fixed? = 101 102 Yes. Version 1.1.0 removes the folder reliably, including on Windows and local servers, and checks again at the end of the request in case Divi recreates it. If your server still blocks deleting the folder (for example because of file ownership), the last clear report shows it as failed instead of pretending it worked. 57 103 58 104 = Can I re-enable Divi's Static CSS File Generation? = 59 105 60 On Divi 4 sites: not while this plugin is active — it's re-enforced on every page load. On Divi 5: there is no longer a setting for this at all (Elegant Themes removed it), so the question doesn't apply — the plugin purges the et-cache folder after every request instead.106 On Divi 4: not while this plugin is active. On Divi 5 the setting no longer exists, so CacheNova simply removes the et-cache folder when you clear the cache. 61 107 62 = Does this fully prevent the et-cache folder from ever existing on Divi 5? =108 = What does "Delete all data on uninstall" delete? = 63 109 64 Not for the full duration of a request — Divi 5 has no opt-out for dynamic CSS generation, so it may briefly create files while serving a page. This plugin removes them again immediately after each request finishes, so the folder never persists or accumulates stale files, but it can't be prevented from being written to at all on Divi 5 the way it can on Divi 4.110 With it ON, deleting the plugin (not just deactivating) removes its settings, reports, schedules and the wp-content/cache/cachenova folder, then runs one final full-site cache clear. It never touches Divi's own et-cache folder. It is OFF by default. You can find it on the Database Optimization tab. 65 111 66 = Does the background schedule slow my site down? =112 = What is the Persistent Object Cache, and is it safe to turn on? = 67 113 68 No — the scheduled background tick only performs a lightweight clear (object cache and expired transients). The heavier full clear only runs when you actually save content in Divi or click "Clear Cache Now" manually. 69 70 = What does "Delete all data on uninstall" actually delete? = 71 72 With that toggle ON, deleting the plugin (not just deactivating it) removes its settings, its last-cleared/last-clear-type records, and the wp-content/cache/cachenova folder it uses for the built-in Public/Private page cache, then runs one final full-site cache clear. It does not touch Divi's own et-cache folder, since that belongs to Divi, not to this plugin. It's OFF by default, so a normal delete leaves your settings in place in case you reinstall later. 73 74 = What is the Persistent Object Cache feature, and is it safe to turn on? = 75 76 A persistent object cache lets WordPress reuse database query results across page loads (via APCu, Redis, or Memcached) instead of re-querying every time, which typically speeds up admin and front-end response times. It's OFF by default. Turning it on runs a real connection test first — nothing is installed if that test fails. If it succeeds, the plugin writes a wp-content/object-cache.php drop-in plus a small config file, and clears the cache once so it starts clean. It will never overwrite a different object-cache.php that's already there (from WP Redis, W3 Total Cache, etc.) — it detects that and asks you to remove the existing one first. Deactivating this plugin always removes the drop-in again, so caching goes back to WordPress's normal per-request behavior the moment you switch it off. 77 78 = Does this work outside of Divi? = 79 80 The cache-plugin detection and general WordPress cache clearing work on any site. The Static CSS File Generation override is Divi-specific and has no effect if Divi isn't active. 114 It lets WordPress reuse database results across page loads using Redis, Memcached or APCu, or a file-based fallback. It is OFF by default and runs a connection test before installing anything. It never overwrites a different object-cache.php that is already there, and deactivating the plugin removes its drop-in again. 81 115 82 116 == Screenshots == … … 87 121 == Changelog == 88 122 89 = 1.0.0 = 90 * Initial public release. Auto-clears transients, object cache, OPcache, and page cache on save or schedule, on any WordPress theme or plugin. 91 * Optional built-in Public/Private page cache with a true early-bypass mode (serves cached pages before WordPress even loads). 92 * Optional persistent object cache with automatic backend detection — Redis, Memcached, APCu, or a zero-dependency file-based fallback — no manual setup required. 93 * Database Optimization tool: clean post revisions, trashed posts, spam and trashed comments, and expired transients, plus a table-optimization pass. Off by default, only runs when explicitly requested. 94 * Automatically refreshes its own cache files whenever the plugin itself is updated, so bug fixes and improvements always take effect instead of a stale copy silently running forever. 95 * If Divi is active, also keeps its et-cache folder from ever accumulating stale files (Divi 4 and Divi 5). 96 * React-based settings dashboard (built on WordPress's own bundled React, no build step) with live status, toast notifications, and a real-time countdown to the next scheduled clear. 97 98 == Upgrade Notice == 123 = 1.1.0 = 124 * Added: Database Optimization with scheduled and manual cleanup. 125 * Added: Improved admin bar cache clearing with toast notifications. 126 * Added: Browser cache refresh through CSS/JS version updates. 127 * Improved: Cache clearing support for Divi, Elementor, WooCommerce, and other builders and cache systems. 128 * Improved: Modern confirmation dialogs and uninstall cleanup. 129 * Fixed: Divi cache deletion, browser cache headers, and object-cache drop-in handling. 130 * Fixed: Cleanup reports and automatic cache-clear scheduling. 99 131 100 132 = 1.0.0 = 101 Initial publicrelease.133 * Initial release. -
cachenova/trunk/templates/advanced-cache-template.php
r3737033 r3777263 1 1 <?php 2 /** 3 * Advanced Cache Name: CacheNova Early-Bypass Page Cache 4 * Installed and managed by the CacheNova plugin. Signature: RLCC_ADVANCED_CACHE_DROPIN 5 */ 2 6 3 7 if ( ! defined( 'ABSPATH' ) ) { -
cachenova/trunk/templates/object-cache-template.php
r3737033 r3777263 1 1 <?php 2 /** 3 * Object Cache Name: CacheNova Persistent Object Cache 4 * Installed and managed by the CacheNova plugin. Signature: RLCC_PERSISTENT_OBJECT_CACHE_DROPIN 5 */ 2 6 3 7 if ( ! defined( 'ABSPATH' ) ) { -
cachenova/trunk/uninstall.php
r3737033 r3777263 88 88 } 89 89 90 $rlcc_cron_hooks = [ 'rlcc_background_clear_event', 'rlcc_deferred_full_clear_event' ];90 $rlcc_cron_hooks = [ 'rlcc_background_clear_event', 'rlcc_deferred_full_clear_event', 'rlcc_db_optimize_event' ]; 91 91 foreach ( $rlcc_cron_hooks as $rlcc_hook ) { 92 92 $rlcc_timestamp = wp_next_scheduled( $rlcc_hook ); … … 103 103 'rlcc_last_clear_type', 104 104 'rlcc_asset_version', 105 'rlcc_last_clear_report', 106 'rlcc_last_db_optimize', 107 'rlcc_installed_version', 108 'rlcc_divi_purge_lock', 105 109 ]; 106 110 foreach ( $rlcc_options as $rlcc_option ) {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)