Changeset 3721983
- Timestamp:
- 09/30/2026 08:06:22 PM (5 weeks ago)
- Location:
- mykavo/trunk
- Files:
-
- 1 added
- 8 edited
-
assets/app.css (modified) (35 diffs)
-
assets/app.js (modified) (19 diffs)
-
includes/class-mykavo-bots.php (added)
-
includes/class-mykavo-connection.php (modified) (2 diffs)
-
includes/class-mykavo-integrations.php (modified) (1 diff)
-
includes/class-mykavo-rest.php (modified) (2 diffs)
-
mykavo.php (modified) (7 diffs)
-
readme.txt (modified) (7 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
mykavo/trunk/assets/app.css
r3712737 r3721983 10 10 11 11 .mk { 12 --mk-ink: #1 51515;12 --mk-ink: #14161a; 13 13 --mk-ink-2: #4a4d55; 14 14 --mk-ink-3: #7a7d86; 15 --mk-line: #e6e4da; 16 --mk-line-2: #efede4; 15 --mk-night: #101216; 16 --mk-night-2: #1b1e24; 17 --mk-night-line: rgb(255 255 255 / 9%); 18 --mk-line: #e7e5dd; 19 --mk-line-2: #f0eee7; 17 20 --mk-card: #ffffff; 18 21 --mk-paper: #fbfaf3; … … 31 34 --mk-info: #5f636c; 32 35 --mk-info-soft: #f0f0ee; 33 --mk-radius: 14px; 34 --mk-radius-sm: 10px; 35 --mk-shadow: 0 1px 2px rgb(21 21 21 / 5%), 0 8px 24px rgb(21 21 21 / 5%); 36 --mk-radius: 16px; 37 --mk-radius-sm: 11px; 38 --mk-shadow: 0 1px 2px rgb(20 22 26 / 4%), 0 10px 30px -14px rgb(20 22 26 / 14%); 39 --mk-shadow-lift: 0 2px 4px rgb(20 22 26 / 5%), 0 18px 40px -16px rgb(20 22 26 / 22%); 40 --mk-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 36 41 37 42 color: var(--mk-ink); 38 43 font-size: 14px; 39 44 line-height: 1.5; 40 max-width: 1180px; 45 font-feature-settings: "cv11", "ss01"; 46 -webkit-font-smoothing: antialiased; 47 max-width: 1200px; 41 48 padding: 20px 0 48px; 42 49 } … … 117 124 } 118 125 119 /* ---------------------------------------------------------------- header -- */ 126 /* ----------------------------------------------------------------- shell -- */ 127 128 /* Header and tabs in one frame: a dark bar with the brand, the site and its 129 status, and a light strip of tabs underneath. */ 130 .mk-shell { 131 margin-bottom: 20px; 132 overflow: visible; 133 border: 1px solid var(--mk-line); 134 border-radius: var(--mk-radius); 135 background: var(--mk-card); 136 box-shadow: var(--mk-shadow); 137 } 120 138 121 139 .mk-top { … … 127 145 } 128 146 147 .mk-shell .mk-top { 148 position: relative; 149 margin: -1px -1px 0; 150 padding: 14px 16px 14px 20px; 151 border-radius: calc(var(--mk-radius) - 1px) calc(var(--mk-radius) - 1px) 0 0; 152 background: 153 radial-gradient(420px 140px at 12% -40%, rgb(255 212 0 / 16%), transparent 70%), 154 var(--mk-night); 155 color: #fff; 156 } 157 129 158 .mk-brand { 130 159 display: inline-flex; … … 132 161 gap: 9px; 133 162 font-size: 19px; 134 font-weight: 650;135 letter-spacing: -0.0 1em;163 font-weight: 700; 164 letter-spacing: -0.02em; 136 165 color: var(--mk-ink); 166 } 167 168 .mk-shell .mk-brand { 169 color: #fff; 137 170 } 138 171 … … 142 175 color: var(--mk-gold); 143 176 filter: drop-shadow(0 0 0.5px rgb(21 21 21 / 60%)); 177 } 178 179 .mk-shell .mk-brand svg { 180 filter: drop-shadow(0 0 10px rgb(255 212 0 / 35%)); 144 181 } 145 182 … … 157 194 } 158 195 196 .mk-shell .mk-site { 197 border-color: var(--mk-night-line); 198 background: rgb(255 255 255 / 6%); 199 color: rgb(255 255 255 / 62%); 200 } 201 159 202 .mk-site b { 160 203 font-weight: 600; … … 162 205 } 163 206 207 .mk-shell .mk-site b { 208 color: #fff; 209 } 210 164 211 .mk-site .mk-dot { 165 212 flex-shrink: 0; … … 173 220 } 174 221 222 .mk-shell .mk-top-actions .mk-btn { 223 border-color: var(--mk-night-line); 224 background: rgb(255 255 255 / 6%); 225 color: rgb(255 255 255 / 86%); 226 } 227 228 .mk-shell .mk-top-actions .mk-btn:hover { 229 border-color: rgb(255 255 255 / 22%); 230 background: rgb(255 255 255 / 12%); 231 color: #fff; 232 } 233 175 234 /* ------------------------------------------------------------------ tabs -- */ 176 235 177 236 .mk-tabs { 178 237 display: flex; 179 gap: 4px; 180 margin-bottom: 18px; 181 padding: 4px; 182 width: max-content; 183 max-width: 100%; 238 gap: 2px; 239 padding: 0 12px; 184 240 overflow-x: auto; 185 border: 1px solid var(--mk-line); 186 border-radius: 999px; 187 background: var(--mk-card); 241 scrollbar-width: none; 242 } 243 244 .mk-tabs::-webkit-scrollbar { 245 display: none; 188 246 } 189 247 190 248 .mk-tab { 249 position: relative; 191 250 display: inline-flex; 192 251 align-items: center; 193 gap: 7px;194 height: 34px;195 padding: 0 1 6px;252 gap: 8px; 253 height: 48px; 254 padding: 0 12px; 196 255 border: 0; 197 border-radius: 999px;198 256 background: transparent; 199 color: var(--mk-ink- 2);257 color: var(--mk-ink-3); 200 258 font-size: 13.5px; 201 font-weight: 550;259 font-weight: 600; 202 260 white-space: nowrap; 203 261 cursor: pointer; 204 transition: background-color 0.15s ease, color 0.15s ease; 262 transition: color 0.15s ease; 263 } 264 265 .mk-tab svg { 266 width: 16px; 267 height: 16px; 268 flex-shrink: 0; 269 opacity: 0.8; 270 } 271 272 .mk-tab::after { 273 content: ""; 274 position: absolute; 275 right: 10px; 276 bottom: 0; 277 left: 10px; 278 height: 3px; 279 border-radius: 3px 3px 0 0; 280 background: transparent; 281 transition: background-color 0.15s ease; 205 282 } 206 283 … … 210 287 211 288 .mk-tab[aria-selected="true"] { 212 background: var(--mk-ink); 213 color: #fff; 289 color: var(--mk-ink); 290 } 291 292 .mk-tab[aria-selected="true"] svg { 293 opacity: 1; 294 } 295 296 .mk-tab[aria-selected="true"]::after { 297 background: var(--mk-gold); 298 box-shadow: 0 -1px 0 var(--mk-ink) inset; 214 299 } 215 300 … … 219 304 padding: 0 6px; 220 305 border-radius: 999px; 221 background: var(--mk- gold);222 color: var(--mk- ink);223 font-size: 11 .5px;306 background: var(--mk-ink); 307 color: var(--mk-gold); 308 font-size: 11px; 224 309 font-weight: 700; 225 310 line-height: 20px; 226 311 text-align: center; 312 font-variant-numeric: tabular-nums; 227 313 } 228 314 … … 337 423 justify-content: space-between; 338 424 gap: 12px; 339 padding: 16px 22px; 425 min-height: 56px; 426 padding: 12px 22px; 340 427 border-bottom: 1px solid var(--mk-line-2); 341 428 } 342 429 343 430 .mk-card-title { 431 display: inline-flex; 432 align-items: center; 344 433 font-size: 14.5px; 345 font-weight: 650;346 letter-spacing: -0.0 05em;434 font-weight: 700; 435 letter-spacing: -0.01em; 347 436 } 348 437 … … 385 474 /* ------------------------------------------------------------------ hero -- */ 386 475 476 /* The status panel: verdict and actions, the open changes by severity, and 477 the site's vital signs, in one card. Its tone colours the medallion and a 478 soft wash in the corner. */ 387 479 .mk-hero { 388 480 position: relative; 389 display: flex;390 gap: 22px;391 align-items: center;392 justify-content: space-between;393 flex-wrap: wrap;394 padding: 26px 26px 24px;395 481 overflow: hidden; 482 padding: 0; 396 483 } 397 484 … … 399 486 content: ""; 400 487 position: absolute; 401 inset: 0 auto 0 0; 402 width: 5px; 403 background: var(--mk-tone, var(--mk-good)); 488 top: -120px; 489 right: -80px; 490 width: 420px; 491 height: 300px; 492 background: radial-gradient(closest-side, var(--mk-tone-soft, var(--mk-good-soft)), transparent); 493 opacity: 0.9; 494 pointer-events: none; 495 } 496 497 .mk-hero-top { 498 position: relative; 499 display: flex; 500 gap: 22px; 501 align-items: center; 502 justify-content: space-between; 503 flex-wrap: wrap; 504 padding: 28px 28px 24px; 404 505 } 405 506 … … 416 517 place-items: center; 417 518 flex-shrink: 0; 418 width: 52px; 419 height: 52px; 420 border-radius: 16px; 421 background: var(--mk-tone-soft, var(--mk-good-soft)); 422 color: var(--mk-tone, var(--mk-good)); 519 width: 56px; 520 height: 56px; 521 border-radius: 18px; 522 background: var(--mk-tone, var(--mk-good)); 523 color: #fff; 524 box-shadow: 0 10px 24px -10px var(--mk-tone, var(--mk-good)), inset 0 1px 0 rgb(255 255 255 / 25%); 525 } 526 527 .mk-tone-busy .mk-hero-icon { 528 background: var(--mk-gold); 529 color: var(--mk-ink); 423 530 } 424 531 … … 429 536 430 537 .mk-eyebrow { 431 margin-bottom: 4px !important; 432 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 538 display: flex; 539 align-items: center; 540 gap: 7px; 541 margin-bottom: 6px !important; 542 font-family: var(--mk-mono); 433 543 font-size: 11px; 434 544 font-weight: 600; … … 438 548 } 439 549 550 .mk-pulse { 551 position: relative; 552 width: 7px; 553 height: 7px; 554 border-radius: 50%; 555 background: var(--mk-tone, var(--mk-good)); 556 } 557 558 .mk-pulse::after { 559 content: ""; 560 position: absolute; 561 inset: -4px; 562 border-radius: 50%; 563 border: 2px solid var(--mk-tone, var(--mk-good)); 564 opacity: 0; 565 animation: mk-ping 2.2s cubic-bezier(0, 0, 0.2, 1) infinite; 566 } 567 568 @keyframes mk-ping { 569 0% { 570 transform: scale(0.6); 571 opacity: 0.55; 572 } 573 574 80%, 575 100% { 576 transform: scale(1.6); 577 opacity: 0; 578 } 579 } 580 440 581 .mk-hero h2 { 441 font-size: 2 4px;442 font-weight: 7 00;443 line-height: 1. 2;444 letter-spacing: -0.02 em;582 font-size: 26px; 583 font-weight: 750; 584 line-height: 1.18; 585 letter-spacing: -0.025em; 445 586 } 446 587 … … 452 593 453 594 .mk-hero-side { 595 position: relative; 454 596 display: flex; 455 597 flex-direction: column; 456 598 align-items: flex-end; 457 gap: 8px;599 gap: 10px; 458 600 } 459 601 … … 502 644 height: 100%; 503 645 border-radius: 999px; 504 background: var(--mk-ink);646 background: linear-gradient(90deg, var(--mk-ink), #3a3d44); 505 647 transition: width 0.4s ease; 506 648 } … … 521 663 } 522 664 523 /* ----------------------------------------------------------------- stats -- */ 665 /* Open changes as one stacked bar, with a button per severity. */ 666 .mk-sevbar-wrap { 667 position: relative; 668 display: flex; 669 flex-wrap: wrap; 670 align-items: center; 671 gap: 10px 18px; 672 padding: 0 28px 22px; 673 } 674 675 .mk-sevbar { 676 display: flex; 677 flex: 1 1 260px; 678 gap: 3px; 679 height: 8px; 680 min-width: 180px; 681 } 682 683 .mk-sevbar span { 684 border-radius: 999px; 685 } 686 687 .mk-sevbar-CRITICAL, 688 .mk-sevbar-item-CRITICAL b { 689 background: var(--mk-crit); 690 } 691 692 .mk-sevbar-HIGH, 693 .mk-sevbar-item-HIGH b { 694 background: var(--mk-high); 695 } 696 697 .mk-sevbar-MEDIUM, 698 .mk-sevbar-item-MEDIUM b { 699 background: #c99400; 700 } 701 702 .mk-sevbar-LOW, 703 .mk-sevbar-item-LOW b { 704 background: var(--mk-low); 705 } 706 707 .mk-sevbar-INFO, 708 .mk-sevbar-item-INFO b { 709 background: #9a9ca3; 710 } 711 712 .mk-sevbar-legend { 713 display: flex; 714 flex-wrap: wrap; 715 gap: 6px; 716 } 717 718 .mk-sevbar-item { 719 display: inline-flex; 720 align-items: center; 721 gap: 7px; 722 height: 28px; 723 padding: 0 10px 0 4px; 724 border: 1px solid var(--mk-line); 725 border-radius: 999px; 726 background: var(--mk-card); 727 color: var(--mk-ink-2); 728 font-size: 12px; 729 font-weight: 600; 730 cursor: pointer; 731 transition: border-color 0.15s ease, transform 0.1s ease; 732 } 733 734 .mk-sevbar-item:hover { 735 border-color: var(--mk-ink); 736 color: var(--mk-ink); 737 } 738 739 .mk-sevbar-item b { 740 min-width: 20px; 741 height: 20px; 742 padding: 0 6px; 743 border-radius: 999px; 744 color: #fff; 745 font-size: 11px; 746 line-height: 20px; 747 text-align: center; 748 font-variant-numeric: tabular-nums; 749 } 750 751 /* --------------------------------------------------------------- vitals -- */ 524 752 525 753 .mk-stats { … … 529 757 } 530 758 531 .mk-stat { 759 /* In the status panel: a strip of four, divided by hairlines. */ 760 .mk-hero-metrics { 761 position: relative; 762 gap: 0; 763 border-top: 1px solid var(--mk-line-2); 764 background: linear-gradient(180deg, var(--mk-paper), var(--mk-card)); 765 } 766 767 .mk-hero-metrics .mk-stat { 768 padding: 16px 22px 18px 28px; 769 } 770 771 .mk-hero-metrics .mk-stat + .mk-stat { 772 border-left: 1px solid var(--mk-line-2); 773 } 774 775 /* On their own (AI crawlers tab): one card each. */ 776 .mk-stats-cards .mk-stat { 532 777 padding: 16px 18px; 778 border: 1px solid var(--mk-line); 779 border-radius: var(--mk-radius); 780 background: var(--mk-card); 781 box-shadow: var(--mk-shadow); 533 782 } 534 783 … … 539 788 color: var(--mk-ink-3); 540 789 font-size: 12px; 541 font-weight: 550;790 font-weight: 600; 542 791 } 543 792 … … 549 798 .mk-stat-value { 550 799 margin-top: 6px !important; 551 font-size: 2 2px;552 font-weight: 7 00;553 letter-spacing: -0.02 em;800 font-size: 24px; 801 font-weight: 750; 802 letter-spacing: -0.025em; 554 803 font-variant-numeric: tabular-nums; 555 804 } 556 805 557 806 .mk-stat-value small { 558 margin-left: 2px;807 margin-left: 3px; 559 808 color: var(--mk-ink-3); 560 809 font-size: 13px; 561 font-weight: 550; 810 font-weight: 600; 811 letter-spacing: 0; 562 812 } 563 813 … … 615 865 background: var(--mk-info-soft); 616 866 color: var(--mk-info); 617 }618 619 .mk-breakdown {620 display: grid;621 grid-template-columns: repeat(5, minmax(0, 1fr));622 gap: 8px;623 padding: 16px 22px 20px;624 }625 626 .mk-breakdown-item {627 padding: 10px 12px;628 border-radius: var(--mk-radius-sm);629 background: var(--mk-paper);630 text-align: left;631 }632 633 .mk-breakdown-item b {634 display: block;635 margin-top: 4px;636 font-size: 20px;637 font-weight: 700;638 font-variant-numeric: tabular-nums;639 }640 641 .mk-breakdown-item.is-zero b {642 color: #b6b4aa;643 867 } 644 868 … … 695 919 } 696 920 921 .mk-row { 922 position: relative; 923 } 924 925 .mk-row::before { 926 content: ""; 927 position: absolute; 928 top: 10px; 929 bottom: 10px; 930 left: 0; 931 width: 3px; 932 border-radius: 0 3px 3px 0; 933 background: var(--mk-gold); 934 opacity: 0; 935 transition: opacity 0.15s ease; 936 } 937 697 938 .mk-row:hover { 698 939 background: var(--mk-paper); 940 } 941 942 .mk-row:hover::before, 943 .mk-row:focus-visible::before { 944 opacity: 1; 945 } 946 947 .mk-row:hover .mk-row-chevron { 948 color: var(--mk-ink); 949 transform: translateX(2px); 950 } 951 952 .mk-row-path { 953 padding: 0; 954 background: none; 955 font-family: var(--mk-mono); 956 font-size: 13px; 957 font-weight: 600; 958 color: var(--mk-ink); 699 959 } 700 960 … … 747 1007 flex-shrink: 0; 748 1008 color: #b6b4aa; 1009 transition: color 0.15s ease, transform 0.15s ease; 749 1010 } 750 1011 … … 935 1196 padding: 22px 24px 18px; 936 1197 border-bottom: 1px solid var(--mk-line); 937 background: var(--mk-card); 1198 background: 1199 radial-gradient(360px 120px at 0% 0%, rgb(255 212 0 / 10%), transparent 70%), 1200 var(--mk-card); 938 1201 } 939 1202 940 1203 .mk-drawer-head h2 { 941 font-size: 19px;942 font-weight: 7 00;943 line-height: 1. 3;944 letter-spacing: -0.0 15em;1204 font-size: 21px; 1205 font-weight: 750; 1206 line-height: 1.25; 1207 letter-spacing: -0.02em; 945 1208 } 946 1209 … … 960 1223 border-top: 1px solid var(--mk-line); 961 1224 background: var(--mk-card); 1225 box-shadow: 0 -10px 24px -18px rgb(20 22 26 / 30%); 962 1226 } 963 1227 … … 973 1237 background: var(--mk-card); 974 1238 cursor: pointer; 1239 transition: border-color 0.15s ease, transform 0.15s ease; 1240 } 1241 1242 .mk-close:hover { 1243 border-color: var(--mk-ink); 1244 transform: rotate(90deg); 975 1245 } 976 1246 … … 1010 1280 1011 1281 .mk-value { 1012 padding: 12px 14px; 1282 position: relative; 1283 padding: 12px 14px 12px 17px; 1284 overflow: hidden; 1013 1285 border-radius: var(--mk-radius-sm); 1014 1286 border: 1px solid var(--mk-line); 1015 1287 background: var(--mk-card); 1288 } 1289 1290 .mk-value::before { 1291 content: ""; 1292 position: absolute; 1293 inset: 0 auto 0 0; 1294 width: 3px; 1295 } 1296 1297 .mk-value-before::before { 1298 background: var(--mk-crit); 1299 } 1300 1301 .mk-value-after::before { 1302 background: var(--mk-good); 1016 1303 } 1017 1304 … … 1505 1792 } 1506 1793 1507 .mk-breakdown { 1508 grid-template-columns: repeat(3, minmax(0, 1fr)); 1794 .mk-hero-metrics .mk-stat { 1795 padding: 14px 16px; 1796 } 1797 1798 .mk-hero-metrics .mk-stat:nth-child(odd) { 1799 border-left: 0; 1800 } 1801 1802 .mk-hero-metrics .mk-stat:nth-child(n + 3) { 1803 border-top: 1px solid var(--mk-line-2); 1804 } 1805 1806 .mk-hero-top { 1807 padding: 22px 18px 18px; 1808 } 1809 1810 .mk-hero h2 { 1811 font-size: 22px; 1812 } 1813 1814 .mk-hero-icon { 1815 width: 46px; 1816 height: 46px; 1817 border-radius: 15px; 1818 } 1819 1820 .mk-sevbar-wrap { 1821 padding: 0 18px 18px; 1822 } 1823 1824 .mk-shell .mk-top { 1825 padding: 12px 12px 12px 14px; 1826 } 1827 1828 .mk-shell .mk-site { 1829 order: 3; 1830 width: 100%; 1831 justify-content: flex-start; 1832 } 1833 1834 .mk-tab { 1835 height: 44px; 1836 padding: 0 10px; 1837 } 1838 1839 /* Rows: severity and time on top, the title below - titles line up. */ 1840 .mk-row:has(> .mk-sev) { 1841 display: grid; 1842 grid-template-columns: auto minmax(0, 1fr) auto; 1843 align-items: center; 1844 gap: 6px 10px; 1845 } 1846 1847 .mk-row:has(> .mk-sev) > .mk-row-main { 1848 grid-column: 1 / -1; 1849 grid-row: 2; 1850 } 1851 1852 .mk-row:has(> .mk-sev) > .mk-row-side { 1853 display: block; 1854 grid-column: 2; 1855 grid-row: 1; 1856 text-align: left; 1857 } 1858 1859 .mk-row:has(> .mk-sev) > .mk-row-chevron { 1860 grid-column: 3; 1861 grid-row: 1; 1862 } 1863 1864 .mk-row-title { 1865 white-space: normal; 1509 1866 } 1510 1867 … … 1535 1892 .mk-row-side { 1536 1893 display: none; 1894 font-size: 12px; 1537 1895 } 1538 1896 … … 1552 1910 1553 1911 @media (prefers-reduced-motion: reduce) { 1912 .mk-pulse::after { 1913 display: none; 1914 } 1915 1554 1916 .mk *, 1555 1917 .mk *::before, … … 1576 1938 width: 16px; 1577 1939 height: 16px; 1578 margin-right: 7px; 1940 flex-shrink: 0; 1941 margin-right: 8px; 1579 1942 vertical-align: -3px; 1580 color: var(--mk- good);1943 color: var(--mk-ink-3); 1581 1944 } 1582 1945 … … 2001 2364 font-size: 12px; 2002 2365 } 2366 2367 /* AI crawlers */ 2368 2369 .mk-bars { 2370 display: flex; 2371 align-items: flex-end; 2372 gap: 3px; 2373 height: 120px; 2374 padding: 18px 22px 20px; 2375 } 2376 2377 .mk-bar { 2378 flex: 1 1 0; 2379 min-width: 3px; 2380 border-radius: 3px 3px 0 0; 2381 background: var(--mk-ink); 2382 } 2383 2384 .mk-bar.is-zero { 2385 background: var(--mk-line); 2386 } 2387 2388 .mk-bad { 2389 color: var(--mk-crit); 2390 font-weight: 650; 2391 } 2392 2393 .mk-path { 2394 display: inline-block; 2395 max-width: 420px; 2396 overflow: hidden; 2397 font-size: 12px; 2398 text-overflow: ellipsis; 2399 white-space: nowrap; 2400 vertical-align: bottom; 2401 } 2402 2403 .mk-cell-agents { 2404 color: var(--mk-ink-2); 2405 font-size: 12px; 2406 } 2407 2408 .mk-bots-note { 2409 padding: 0 4px; 2410 line-height: 1.55; 2411 } 2412 2413 .mk-bots-total { 2414 color: var(--mk-ink-2); 2415 font-size: 13px; 2416 } 2417 2418 .mk-bots-total b { 2419 color: var(--mk-ink); 2420 font-size: 22px; 2421 font-weight: 700; 2422 letter-spacing: -0.01em; 2423 } 2424 2425 .mk-bots-mini { 2426 margin: 10px 0 0 !important; 2427 padding: 0; 2428 list-style: none; 2429 } 2430 2431 .mk-bots-mini li { 2432 display: flex; 2433 justify-content: space-between; 2434 gap: 12px; 2435 margin: 0; 2436 padding: 6px 0; 2437 border-top: 1px solid var(--mk-line-2); 2438 font-size: 13px; 2439 } 2440 2441 .mk-bots-mini em { 2442 color: var(--mk-ink-3); 2443 font-size: 12px; 2444 font-style: normal; 2445 } -
mykavo/trunk/assets/app.js
r3712737 r3721983 39 39 updates: null, 40 40 updatesError: null, 41 bots: null, 42 botsError: null, 41 43 changesScan: null, 42 44 addUrl: '', … … 203 205 plug: '<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/>', 204 206 brush: '<path d="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/>', 207 bot: '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>', 205 208 wp: '<circle cx="12" cy="12" r="10"/><path d="M3.5 8.5 8 20l3-8.5"/><path d="M9 8.5h5"/><path d="M11.5 8.5 16 20l4.5-11.5"/>', 206 209 }; … … 384 387 state.updates = data; 385 388 toast( enabled ? __( 'Update checks are on.', 'mykavo' ) : __( 'Update checks are off.', 'mykavo' ) ); 389 render(); 390 } ); 391 } 392 393 function loadBots() { 394 return api( '/bots' ) 395 .then( function ( data ) { 396 state.bots = data; 397 state.botsError = null; 398 render(); 399 } ) 400 .catch( function ( err ) { 401 state.botsError = errorMessage( err ); 402 render(); 403 } ); 404 } 405 406 function setBotCounting( enabled ) { 407 api( '/bots', { method: 'POST', data: { enabled: enabled } } ).then( function ( data ) { 408 state.bots = data; 409 toast( enabled ? __( 'Counting AI crawler visits.', 'mykavo' ) : __( 'AI crawler counting is off.', 'mykavo' ) ); 386 410 render(); 387 411 } ); … … 439 463 } else if ( state.tab === 'pages' && ! state.pages ) { 440 464 loadPages(); 465 } else if ( state.tab === 'bots' ) { 466 loadBots(); 441 467 } else if ( state.tab === 'updates' ) { 442 468 if ( ! state.updates ) { … … 677 703 } 678 704 705 var TAB_ICONS = { 706 overview: 'gauge', 707 changes: 'eye', 708 updates: 'shield', 709 bots: 'bot', 710 scans: 'clock', 711 pages: 'file', 712 }; 713 679 714 function tabs() { 680 715 var open = state.overview ? state.overview.stats.openChanges : 0; … … 683 718 [ 'changes', __( 'Changes', 'mykavo' ), open > 0 ? '<span class="mk-count">' + esc( open > 99 ? '99+' : open ) + '</span>' : '' ], 684 719 [ 'updates', __( 'Safe Updates', 'mykavo' ), '' ], 720 [ 'bots', __( 'AI crawlers', 'mykavo' ), '' ], 685 721 [ 'scans', __( 'Scans', 'mykavo' ), '' ], 686 722 [ 'pages', __( 'Pages', 'mykavo' ), '' ], … … 690 726 list.map( function ( t ) { 691 727 var selected = state.tab === t[ 0 ]; 692 return '<button type="button" role="tab" class="mk-tab" data-act="tab" data-tab="' + t[ 0 ] + '" data-key="tab-' + t[ 0 ] + '" aria-selected="' + selected + '" tabindex="' + ( selected ? '0' : '-1' ) + '">' + esc( t[ 1 ] )+ t[ 2 ] + '</button>';728 return '<button type="button" role="tab" class="mk-tab" data-act="tab" data-tab="' + t[ 0 ] + '" data-key="tab-' + t[ 0 ] + '" aria-selected="' + selected + '" tabindex="' + ( selected ? '0' : '-1' ) + '">' + icon( TAB_ICONS[ t[ 0 ] ] ) + '<span>' + esc( t[ 1 ] ) + '</span>' + t[ 2 ] + '</button>'; 693 729 } ).join( '' ) + 694 730 '</div>' … … 786 822 return ( 787 823 '<section class="mk-card mk-hero mk-tone-' + tone + '" aria-live="polite">' + 824 '<div class="mk-hero-top">' + 788 825 '<div class="mk-hero-main"><span class="mk-hero-icon">' + icon( ic, ic === 'loader' ? 'mk-spin' : '' ) + '</span>' + 789 '<div style="min-width:0;flex:1"><p class="mk-eyebrow"> ' + esc( eyebrow ) + '</p><h2>' + esc( title ) + '</h2>' +826 '<div style="min-width:0;flex:1"><p class="mk-eyebrow"><span class="mk-pulse" aria-hidden="true"></span>' + esc( eyebrow ) + '</p><h2>' + esc( title ) + '</h2>' + 790 827 ( sub ? '<p class="mk-hero-sub">' + esc( sub ) + '</p>' : '' ) + extra + '</div></div>' + 791 828 '<div class="mk-hero-side"><div class="mk-hero-actions">' + reviewBtn + scanBtn + '</div>' + 792 829 ( ! disabled || ! cap.manualScanBlockedReason ? '' : '<span class="mk-hero-meta">' + esc( cap.manualScanBlockedReason ) + '</span>' ) + 793 830 ( meta.length ? '<span class="mk-hero-meta">' + esc( meta.join( ' · ' ) ) + '</span>' : '' ) + 794 '</div></section>' 831 '</div></div>' + 832 severityBar( o ) + 833 stats( o ) + 834 '</section>' 835 ); 836 } 837 838 /** Open changes as one stacked bar, with a jump into Changes per severity. */ 839 function severityBar( o ) { 840 var by = o.stats.bySeverity; 841 if ( ! o.stats.openChanges ) { 842 return ''; 843 } 844 var present = SEVERITIES.filter( function ( s ) { 845 return by[ s ] > 0; 846 } ); 847 return ( 848 '<div class="mk-sevbar-wrap">' + 849 '<div class="mk-sevbar" role="img" aria-label="' + esc( present.map( function ( s ) { 850 return by[ s ] + ' ' + severityLabel( s ); 851 } ).join( ', ' ) ) + '">' + 852 present.map( function ( s ) { 853 return '<span class="mk-sevbar-' + s + '" style="flex-grow:' + by[ s ] + '"></span>'; 854 } ).join( '' ) + 855 '</div>' + 856 '<div class="mk-sevbar-legend">' + 857 present.map( function ( s ) { 858 return '<button type="button" class="mk-sevbar-item mk-sevbar-item-' + s + '" data-act="severity-jump" data-sev="' + s + '"><b>' + esc( by[ s ] ) + '</b>' + esc( severityLabel( s ) ) + '</button>'; 859 } ).join( '' ) + 860 '</div></div>' 795 861 ); 796 862 } … … 798 864 function stat( ic, label, value, unit, foot ) { 799 865 return ( 800 '<div class="mk- card mk-stat"><p class="mk-stat-label">' + icon( ic ) + esc( label ) + '</p>' +866 '<div class="mk-stat"><p class="mk-stat-label">' + icon( ic ) + esc( label ) + '</p>' + 801 867 '<p class="mk-stat-value">' + esc( value ) + ( unit ? '<small>' + esc( unit ) + '</small>' : '' ) + '</p>' + 802 868 ( foot ? '<p class="mk-stat-foot">' + esc( foot ) + '</p>' : '' ) + '</div>' … … 810 876 }; 811 877 return ( 812 '<div class="mk-stats ">' +878 '<div class="mk-stats mk-hero-metrics">' + 813 879 stat( 'activity', __( 'Uptime, 24 hours', 'mykavo' ), pct( h.uptime24h ), typeof h.uptime24h === 'number' ? '%' : '', typeof h.uptime7d === 'number' ? sprintf( /* translators: %s: percentage. */ __( '%s%% over 7 days', 'mykavo' ), pct( h.uptime7d ) ) : __( 'First check within minutes', 'mykavo' ) ) + 814 880 stat( 'gauge', __( 'Response time', 'mykavo' ), typeof h.avgResponseMs24h === 'number' ? Math.round( h.avgResponseMs24h ) : '-', typeof h.avgResponseMs24h === 'number' ? 'ms' : '', __( 'Average, 24 hours', 'mykavo' ) ) + … … 844 910 return '<div class="mk-grid">' + loadingBlock( 150 ) + loadingBlock( 96 ) + loadingBlock( 260 ) + '</div>'; 845 911 } 846 var by = o.stats.bySeverity;847 912 var top = o.topChanges || []; 848 913 849 914 var attention = 850 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Needs attention', 'mykavo' ) ) + '</h3>' +915 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + icon( 'alert', 'mk-title-icon' ) + esc( __( 'Needs attention', 'mykavo' ) ) + '</h3>' + 851 916 ( o.stats.openChanges > 0 ? '<button type="button" class="mk-link" style="border:0;background:none;cursor:pointer" data-act="tab" data-tab="changes">' + esc( sprintf( /* translators: %d: number of open changes. */ __( 'All %d open changes', 'mykavo' ), o.stats.openChanges ) ) + icon( 'chevron' ) + '</button>' : '' ) + 852 917 '</div>' + … … 856 921 '</section>'; 857 922 858 var breakdown =859 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Open changes by severity', 'mykavo' ) ) + '</h3></div>' +860 '<div class="mk-breakdown">' +861 SEVERITIES.map( function ( s ) {862 return '<div class="mk-breakdown-item' + ( by[ s ] ? '' : ' is-zero' ) + '">' + sevPill( s ) + '<b>' + esc( by[ s ] ) + '</b></div>';863 } ).join( '' ) +864 '</div></section>';865 866 923 var scans = o.recentScans || []; 867 924 var recent = 868 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Recent scans', 'mykavo' ) ) + '</h3>' +925 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + icon( 'clock', 'mk-title-icon' ) + esc( __( 'Recent scans', 'mykavo' ) ) + '</h3>' + 869 926 '<button type="button" class="mk-link" style="border:0;background:none;cursor:pointer" data-act="tab" data-tab="scans">' + esc( __( 'History', 'mykavo' ) ) + icon( 'chevron' ) + '</button></div>' + 870 927 ( scans.length … … 893 950 '<div class="mk-grid">' + 894 951 hero( o ) + 895 stats( o ) + 896 '<div class="mk-grid mk-grid-main"><div class="mk-grid">' + attention + breakdown + '</div><div class="mk-grid">' + storeCard() + safeUpdatesCard() + recent + plan + '</div></div>' + 952 '<div class="mk-grid mk-grid-main"><div class="mk-grid">' + attention + '</div><div class="mk-grid">' + storeCard() + safeUpdatesCard() + aiBotsCard() + recent + plan + '</div></div>' + 897 953 '</div>' 898 954 ); … … 1002 1058 '<li><a class="mk-row" href="' + esc( url ) + '" target="_blank" rel="noopener noreferrer">' + 1003 1059 icon( 'file', 'mk-row-chevron' ) + 1004 '<span class="mk-row-main"><span class="mk-row-title">' + esc( p.name || pathOf( p.url )) + '</span>' +1005 '<span class="mk-row-meta"> <code>' + esc( pathOf( p.url ) ) + '</code>'+1060 '<span class="mk-row-main"><span class="mk-row-title">' + ( p.name ? esc( p.name ) : '<code class="mk-row-path">' + esc( pathOf( p.url ) ) + '</code>' ) + '</span>' + 1061 '<span class="mk-row-meta">' + ( p.name ? '<code>' + esc( pathOf( p.url ) ) + '</code>' : '' ) + 1006 1062 ( p.baselineVersion ? '<span>' + esc( sprintf( /* translators: %d: baseline version. */ __( 'Baseline v%d', 'mykavo' ), p.baselineVersion ) ) + '</span>' : '<span>' + esc( __( 'No baseline yet', 'mykavo' ) ) + '</span>' ) + 1007 1063 ( p.enabled ? '' : '<span class="mk-status">' + esc( __( 'Paused', 'mykavo' ) ) + '</span>' ) + … … 1283 1339 } 1284 1340 1341 /* ------------------------------------------------------- AI crawlers -- */ 1342 1343 function utcDay( offsetDays ) { 1344 return new Date( Date.now() - ( offsetDays || 0 ) * 86400000 ).toISOString().slice( 0, 10 ); 1345 } 1346 1347 function dayLabel( day ) { 1348 if ( day === utcDay( 0 ) ) { 1349 return __( 'Today', 'mykavo' ); 1350 } 1351 if ( day === utcDay( 1 ) ) { 1352 return __( 'Yesterday', 'mykavo' ); 1353 } 1354 var d = new Date( day + 'T00:00:00Z' ); 1355 return isNaN( d.getTime() ) ? day : d.toLocaleDateString( locale, { month: 'short', day: 'numeric', timeZone: 'UTC' } ); 1356 } 1357 1358 function purposeLabel( purpose ) { 1359 return { 1360 search: __( 'Reads pages for AI search answers', 'mykavo' ), 1361 user: __( 'Opens a page a person asked about', 'mykavo' ), 1362 training: __( 'Collects training data', 'mykavo' ), 1363 }[ purpose ] || ''; 1364 } 1365 1366 function num( n ) { 1367 return ( Number( n ) || 0 ).toLocaleString( locale ); 1368 } 1369 1370 function aiBotsCard() { 1371 var b = state.bots; 1372 if ( ! b ) { 1373 return ''; 1374 } 1375 var top = ( b.agents || [] ).slice( 0, 3 ); 1376 var body = ! b.enabled 1377 ? '<p class="mk-stat-foot" style="font-size:13px">' + esc( __( 'Counting is off.', 'mykavo' ) ) + '</p>' 1378 : ! b.total 1379 ? '<p class="mk-stat-foot" style="font-size:13px">' + esc( __( 'No AI crawler visits counted yet. ChatGPT, Claude and Perplexity usually show up within a few days.', 'mykavo' ) ) + '</p>' 1380 : '<p class="mk-bots-total"><b>' + esc( num( b.total ) ) + '</b> ' + esc( __( 'visits in 30 days', 'mykavo' ) ) + '</p>' + 1381 '<ul class="mk-bots-mini">' + top.map( function ( a ) { 1382 return '<li><span>' + esc( a.owner ) + ' <em>' + esc( a.agent ) + '</em></span><b>' + esc( num( a.hits ) ) + '</b></li>'; 1383 } ).join( '' ) + '</ul>'; 1384 return ( 1385 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + icon( 'bot', 'mk-title-icon' ) + esc( __( 'AI crawlers', 'mykavo' ) ) + '</h3>' + 1386 '<button type="button" class="mk-link" style="border:0;background:none;cursor:pointer" data-act="tab" data-tab="bots">' + esc( __( 'Details', 'mykavo' ) ) + icon( 'chevron' ) + '</button></div>' + 1387 '<div class="mk-card-pad" style="padding-top:12px">' + body + '</div></section>' 1388 ); 1389 } 1390 1391 function botsView() { 1392 if ( state.botsError && ! state.bots ) { 1393 return errorBlock( state.botsError, 'retry-bots' ); 1394 } 1395 if ( ! state.bots ) { 1396 return loadingBlock( 260 ); 1397 } 1398 var b = state.bots; 1399 var on = !! b.enabled; 1400 var intro = 1401 '<section class="mk-card mk-safe">' + 1402 '<div class="mk-safe-copy"><span class="mk-safe-icon">' + icon( 'bot' ) + '</span><div>' + 1403 '<h2>' + esc( __( 'Which AI reads your site', 'mykavo' ) ) + '</h2>' + 1404 '<p>' + esc( __( 'ChatGPT, Claude, Perplexity and other AI systems send crawlers to read your pages before they quote or recommend them. MyKavo counts those visits - by crawler name only: no IP addresses, no cookies, nothing about human visitors.', 'mykavo' ) ) + '</p>' + 1405 '</div></div>' + 1406 '<button type="button" class="mk-switch" role="switch" aria-checked="' + on + '" data-act="toggle-bots" data-key="toggle-bots">' + 1407 '<span class="mk-switch-track"><span class="mk-switch-thumb"></span></span>' + 1408 '<span>' + esc( on ? __( 'Counting on', 'mykavo' ) : __( 'Counting off', 'mykavo' ) ) + '</span></button>' + 1409 '</section>'; 1410 1411 if ( ! b.ready ) { 1412 return '<div class="mk-grid">' + intro + '<div class="mk-banner">' + icon( 'alert' ) + '<span>' + 1413 esc( __( 'MyKavo could not create its small visit-count table in this site\'s database. Your hosting may not allow new tables. It retries once a day.', 'mykavo' ) ) + '</span></div></div>'; 1414 } 1415 1416 var foot = 1417 '<p class="mk-fine mk-bots-note">' + 1418 esc( __( 'Counts visits that reach this site\'s server. Pages served straight from a page cache or CDN may not be counted, so real numbers can be higher. Crawlers are identified by the name they send.', 'mykavo' ) ) + 1419 ( b.sentAt ? ' ' + esc( sprintf( /* translators: %s: relative time, e.g. "3 hours ago". */ __( 'Daily totals are shared with your MyKavo dashboard (last sent %s).', 'mykavo' ), rel( b.sentAt ) ) ) : ' ' + esc( __( 'Daily totals are shared with your MyKavo dashboard once a day.', 'mykavo' ) ) ) + 1420 '</p>'; 1421 1422 if ( ! b.total ) { 1423 return '<div class="mk-grid">' + intro + 1424 '<section class="mk-card"><div class="mk-empty"><span class="mk-empty-icon">' + icon( 'bot' ) + '</span><strong>' + 1425 esc( on ? __( 'No AI crawler visits yet', 'mykavo' ) : __( 'Counting is off', 'mykavo' ) ) + '</strong><span>' + 1426 esc( on 1427 ? __( 'Visits appear here as they happen. ChatGPT, Claude and Perplexity usually show up within a few days. If none ever do, check that robots.txt does not block them.', 'mykavo' ) 1428 : __( 'Switch counting on to see which AI systems read your site.', 'mykavo' ) ) + 1429 '</span></div></section>' + foot + '</div>'; 1430 } 1431 1432 var answerHits = 0; 1433 ( b.agents || [] ).forEach( function ( a ) { 1434 if ( a.purpose !== 'training' ) { 1435 answerHits += a.hits; 1436 } 1437 } ); 1438 var stats = 1439 '<div class="mk-stats mk-stats-cards">' + 1440 stat( 'activity', __( 'AI crawler visits', 'mykavo' ), num( b.total ), '', __( 'Last 30 days', 'mykavo' ) ) + 1441 stat( 'eye', __( 'For AI answers', 'mykavo' ), num( answerHits ), '', __( 'Search and "read this page" visits', 'mykavo' ) ) + 1442 stat( 'bot', __( 'Crawlers seen', 'mykavo' ), b.agents.length, '', __( 'Different AI crawlers', 'mykavo' ) ) + 1443 stat( 'alert', __( 'Errors served', 'mykavo' ), num( b.errors ), '', __( 'Visits that got a 4xx or 5xx', 'mykavo' ) ) + 1444 '</div>'; 1445 1446 var max = 1; 1447 b.daily.forEach( function ( d ) { 1448 max = Math.max( max, d.hits ); 1449 } ); 1450 var chart = 1451 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Visits per day', 'mykavo' ) ) + '</h3>' + 1452 '<span class="mk-fine">' + esc( __( 'Last 30 days (UTC)', 'mykavo' ) ) + '</span></div>' + 1453 '<div class="mk-bars" role="img" aria-label="' + esc( sprintf( /* translators: %s: number of visits. */ __( '%s AI crawler visits over the last 30 days', 'mykavo' ), num( b.total ) ) ) + '">' + 1454 b.daily.map( function ( d ) { 1455 var h = d.hits ? Math.max( 4, Math.round( ( d.hits / max ) * 100 ) ) : 0; 1456 return '<span class="mk-bar' + ( d.hits ? '' : ' is-zero' ) + '" style="height:' + ( d.hits ? h : 3 ) + '%" title="' + esc( dayLabel( d.day ) + ': ' + num( d.hits ) ) + '"></span>'; 1457 } ).join( '' ) + 1458 '</div></section>'; 1459 1460 var crawlers = 1461 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Crawlers', 'mykavo' ) ) + '</h3></div>' + 1462 '<div class="mk-table-wrap"><table class="mk-table"><thead><tr>' + 1463 '<th>' + esc( __( 'Crawler', 'mykavo' ) ) + '</th><th>' + esc( __( 'Why it visits', 'mykavo' ) ) + '</th>' + 1464 '<th class="mk-num">' + esc( __( 'Visits', 'mykavo' ) ) + '</th><th class="mk-num">' + esc( __( 'Errors', 'mykavo' ) ) + '</th><th>' + esc( __( 'Last seen', 'mykavo' ) ) + '</th>' + 1465 '</tr></thead><tbody>' + 1466 b.agents.map( function ( a ) { 1467 return '<tr><td><b>' + esc( a.owner ) + '</b><span class="mk-cell-note"><code>' + esc( a.agent ) + '</code></span></td>' + 1468 '<td>' + esc( purposeLabel( a.purpose ) ) + '</td>' + 1469 '<td class="mk-num">' + esc( num( a.hits ) ) + '</td>' + 1470 '<td class="mk-num' + ( a.errors ? ' mk-bad' : '' ) + '">' + esc( num( a.errors ) ) + '</td>' + 1471 '<td>' + esc( dayLabel( a.lastSeen ) ) + '</td></tr>'; 1472 } ).join( '' ) + 1473 '</tbody></table></div></section>'; 1474 1475 var pages = 1476 '<section class="mk-card"><div class="mk-card-head"><h3 class="mk-card-title">' + esc( __( 'Pages AI reads most', 'mykavo' ) ) + '</h3>' + 1477 '<span class="mk-fine">' + esc( __( 'Last 30 days', 'mykavo' ) ) + '</span></div>' + 1478 '<div class="mk-table-wrap"><table class="mk-table"><thead><tr>' + 1479 '<th>' + esc( __( 'Page', 'mykavo' ) ) + '</th><th class="mk-num">' + esc( __( 'Visits', 'mykavo' ) ) + '</th><th class="mk-num">' + esc( __( 'Errors', 'mykavo' ) ) + '</th><th>' + esc( __( 'By', 'mykavo' ) ) + '</th>' + 1480 '</tr></thead><tbody>' + 1481 b.pages.map( function ( p ) { 1482 return '<tr><td><code class="mk-path">' + esc( p.path ) + '</code></td>' + 1483 '<td class="mk-num">' + esc( num( p.hits ) ) + '</td>' + 1484 '<td class="mk-num' + ( p.errors ? ' mk-bad' : '' ) + '">' + esc( num( p.errors ) ) + '</td>' + 1485 '<td class="mk-cell-agents">' + esc( p.agents.join( ', ' ) ) + '</td></tr>'; 1486 } ).join( '' ) + 1487 '</tbody></table></div></section>'; 1488 1489 return '<div class="mk-grid">' + intro + stats + chart + crawlers + pages + foot + '</div>'; 1490 } 1491 1285 1492 function render() { 1286 1493 var focusKey = document.activeElement && document.activeElement.getAttribute ? document.activeElement.getAttribute( 'data-key' ) : null; … … 1293 1500 var view = state.tab === 'changes' 1294 1501 ? changesView() 1502 : state.tab === 'bots' 1503 ? botsView() 1295 1504 : state.tab === 'updates' 1296 1505 ? updatesView() … … 1300 1509 ? pagesView() 1301 1510 : overviewView(); 1302 root.innerHTML = header() + banner() + tabs() + '<div role="tabpanel">' + view + '</div>' + footer();1511 root.innerHTML = '<div class="mk-shell">' + header() + tabs() + '</div>' + banner() + '<div role="tabpanel" class="mk-panel">' + view + '</div>' + footer(); 1303 1512 1304 1513 if ( focusKey ) { … … 1571 1780 loadScans(); 1572 1781 break; 1782 case 'severity-jump': 1783 state.tab = 'changes'; 1784 state.changesStatus = 'open'; 1785 state.severity = el.getAttribute( 'data-sev' ) || ''; 1786 loadChanges(); 1787 break; 1788 case 'toggle-bots': 1789 setBotCounting( ! ( state.bots && state.bots.enabled ) ); 1790 break; 1791 case 'retry-bots': 1792 state.botsError = null; 1793 state.bots = null; 1794 render(); 1795 loadBots(); 1796 break; 1573 1797 case 'toggle-updates': 1574 1798 setUpdateChecks( ! ( state.updates && state.updates.enabled ) ); … … 1667 1891 loadOverview( false ); 1668 1892 loadUpdates(); 1893 loadBots(); 1669 1894 if ( cfg.woo && cfg.woo.pages && cfg.woo.pages.length ) { 1670 1895 loadPages(); -
mykavo/trunk/includes/class-mykavo-connection.php
r3712737 r3721983 64 64 } 65 65 self::flush_cache(); 66 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 67 MyKavo_Bots::ensure_schedule(); 66 68 } 67 69 … … 75 77 self::flush_cache(); 76 78 delete_transient( self::SUMMARY_KEY ); 79 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 80 MyKavo_Bots::unschedule(); 77 81 } 78 82 -
mykavo/trunk/includes/class-mykavo-integrations.php
r3712737 r3721983 300 300 return; 301 301 } 302 $content = '<p class="privacy-policy-tutorial">' . esc_html__( 'MyKavo collects nothing about your visitors. It adds no scripts, cookies or tracking to your public pages, so there is usually nothing to add to your privacy policy for it. ', 'mykavo' ) . '</p>' .302 $content = '<p class="privacy-policy-tutorial">' . esc_html__( 'MyKavo collects nothing about your visitors. It adds no scripts, cookies or tracking to your public pages, so there is usually nothing to add to your privacy policy for it. It counts visits from AI crawlers such as GPTBot or ClaudeBot by the name they send - never IP addresses or anything about people.', 'mykavo' ) . '</p>' . 303 303 '<p>' . esc_html__( 'This site uses MyKavo (https://mykavo.app) to monitor its public pages for changes. MyKavo loads those pages from its own servers, the same way a visitor would, and does not receive any information about the people who visit this site.', 'mykavo' ) . '</p>'; 304 304 wp_add_privacy_policy_content( 'MyKavo', wp_kses_post( $content ) ); -
mykavo/trunk/includes/class-mykavo-rest.php
r3712737 r3721983 190 190 register_rest_route( 191 191 self::NS, 192 '/bots', 193 array( 194 array( 195 'methods' => WP_REST_Server::READABLE, 196 'callback' => array( __CLASS__, 'bots' ), 197 'permission_callback' => $admin, 198 ), 199 array( 200 'methods' => WP_REST_Server::CREATABLE, 201 'callback' => array( __CLASS__, 'bots_settings' ), 202 'permission_callback' => $admin, 203 'args' => array( 204 'enabled' => array( 205 'type' => 'boolean', 206 'required' => true, 207 ), 208 ), 209 ), 210 ) 211 ); 212 213 register_rest_route( 214 self::NS, 192 215 '/disconnect', 193 216 array( … … 376 399 MyKavo_Updates::set_enabled( (bool) $request->get_param( 'enabled' ) ); 377 400 return self::updates(); 401 } 402 403 /** 404 * AI crawler visits over the last 30 days, counted on this site. 405 * 406 * @return WP_REST_Response 407 */ 408 public static function bots() { 409 return rest_ensure_response( MyKavo_Bots::summary() ); 410 } 411 412 /** 413 * Switch AI crawler counting on or off. 414 * 415 * @param WP_REST_Request $request Request. 416 * @return WP_REST_Response 417 */ 418 public static function bots_settings( WP_REST_Request $request ) { 419 MyKavo_Bots::set_enabled( (bool) $request->get_param( 'enabled' ) ); 420 return self::bots(); 378 421 } 379 422 -
mykavo/trunk/mykavo.php
r3712737 r3721983 4 4 * Plugin URI: https://mykavo.app/wordpress-plugin 5 5 * Description: See what changed on your site, and whether it matters, without leaving WordPress. Visual, SEO, content, link and script changes with before-and-after screenshots. Adds nothing to the pages your visitors load. 6 * Version: 1. 0.06 * Version: 1.2.0 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 7.4 … … 18 18 defined( 'ABSPATH' ) || exit; 19 19 20 define( 'MYKAVO_VERSION', '1. 0.0' );20 define( 'MYKAVO_VERSION', '1.2.0' ); 21 21 define( 'MYKAVO_FILE', __FILE__ ); 22 22 define( 'MYKAVO_DIR', plugin_dir_path( __FILE__ ) ); … … 42 42 * changes. Nothing is loaded until one of those actually runs. Everything else 43 43 * loads inside wp-admin, and remote calls happen only while an administrator 44 * is looking at a MyKavo screen - or once, right after an update. 44 * is looking at a MyKavo screen - or once, right after an update, and once a 45 * day from WP-Cron to share AI crawler totals. 46 * 47 * The one thing that runs on every public request is a single string match 48 * on the User-Agent header, to spot AI crawlers. Human visitors stop there: 49 * no query, no file loaded. See "AI crawler visits" below. 45 50 */ 46 51 if ( is_admin() ) { … … 50 55 require_once MYKAVO_DIR . 'includes/class-mykavo-updates.php'; 51 56 require_once MYKAVO_DIR . 'includes/class-mykavo-integrations.php'; 57 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 52 58 MyKavo_Admin::init(); 59 add_action( 60 'admin_init', 61 static function () { 62 MyKavo_Bots::install(); 63 MyKavo_Bots::ensure_schedule(); 64 } 65 ); 53 66 MyKavo_Integrations::init(); 54 67 } … … 105 118 ); 106 119 120 /* 121 * AI crawler visits. Counts requests from ChatGPT, Claude, Perplexity and 122 * other AI crawlers - by User-Agent only: no IP addresses, no cookies, and 123 * nothing at all about human visitors, who never get past this match. For a 124 * crawler request the recorder loads and adds one counter update once the 125 * response is complete. 126 */ 127 if ( ! is_admin() && isset( $_SERVER['HTTP_USER_AGENT'] ) && is_string( $_SERVER['HTTP_USER_AGENT'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only matched against fixed tokens. 128 if ( preg_match( '/OAI-SearchBot|ChatGPT-User|GPTBot|Claude-SearchBot|Claude-User|ClaudeBot|PerplexityBot|Perplexity-User|Amazonbot|DuckAssistBot|Meta-ExternalAgent|MistralAI-User|CCBot|Bytespider/i', $_SERVER['HTTP_USER_AGENT'], $mykavo_bot ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- as above. 129 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 130 MyKavo_Bots::track( $mykavo_bot[0] ); 131 } 132 } 133 add_action( 134 'mykavo_bots_daily', 135 static function () { 136 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 137 MyKavo_Bots::daily(); 138 } 139 ); 140 107 141 if ( defined( 'WP_CLI' ) && WP_CLI ) { 108 142 require_once MYKAVO_DIR . 'includes/class-mykavo-connection.php'; … … 117 151 static function () { 118 152 set_transient( 'mykavo_just_activated', 1, MINUTE_IN_SECONDS * 10 ); 153 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 154 MyKavo_Bots::install(); 155 MyKavo_Bots::ensure_schedule(); 156 } 157 ); 158 159 register_deactivation_hook( 160 __FILE__, 161 static function () { 162 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 163 MyKavo_Bots::unschedule(); 119 164 } 120 165 ); … … 127 172 require_once MYKAVO_DIR . 'includes/class-mykavo-updates.php'; 128 173 require_once MYKAVO_DIR . 'includes/class-mykavo-integrations.php'; 174 require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php'; 129 175 require_once MYKAVO_DIR . 'includes/class-mykavo-rest.php'; 130 176 MyKavo_Rest::register_routes(); -
mykavo/trunk/readme.txt
r3712737 r3721983 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.07 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 26 26 * **Know before you press Update.** On the Plugins screen, a plugin whose last update changed things on your site is flagged: "Last time this plugin updated, 2 things changed on your site." 27 27 * Update checks are included in the Pro and Agency plans. On the free plan, updates are still listed so you know what changed and when. 28 29 = See which AI reads your site = 30 31 ChatGPT, Claude, Perplexity and other AI systems send crawlers to read your pages before they quote or recommend them. The **AI crawlers** screen shows: 32 33 * **Who visits.** Visits from OAI-SearchBot, ChatGPT-User, GPTBot, Claude-SearchBot, Claude-User, ClaudeBot, PerplexityBot, Perplexity-User, Amazonbot, DuckAssistBot, Meta-ExternalAgent, MistralAI-User, CCBot and Bytespider, and why each one visits (AI search answers, a person asking about your page, or training). 34 * **Which pages AI reads most**, and visits per day for the last 30 days. 35 * **Errors served to AI.** Pages that answered a crawler with 404 or 5xx - pages AI cannot quote. 36 37 Crawlers are counted by the name they send in the User-Agent header. No IP addresses, no cookies and nothing about human visitors are ever recorded. You can switch counting off on that screen. 28 38 29 39 = Built for WooCommerce stores = … … 61 71 Many plugins get deleted because they make a site slower. MyKavo is built so that cannot happen: 62 72 63 * **Nothing on the public site.** No scripts, no styles, no database queries and no remote requests on the pages your visitors load. 73 * **Nothing on the public site.** No scripts, no styles, no database queries and no remote requests on the pages your visitors load. The only thing that runs is one text match on the User-Agent header to spot AI crawlers; human visitors stop there. 74 * **AI crawler counting is one counter update.** A request from an AI crawler adds one row update to a small table, after the page has been built. Rows older than 35 days are deleted. 64 75 * **No autoloaded options.** The connection is stored in a single option that WordPress only reads on MyKavo's own screens. 65 * ** No cron jobs and no custom tables.** The scanningruns on MyKavo's servers, not yours.76 * **One small daily job.** Once a day, WP-Cron sends the AI crawler totals to MyKavo. The scanning itself runs on MyKavo's servers, not yours. 66 77 * **Admin screens only.** Scripts load on the MyKavo screen alone (plus a 2 KB script for the Dashboard widget), using only libraries WordPress already includes. 67 78 * **Update checks cost nothing extra.** The plugin notes versions while WordPress is updating, then sends one short report when the update finishes. Nothing runs between updates. … … 90 101 * **While an administrator views a MyKavo screen or runs a `wp mykavo` command:** your site's server requests this website's monitoring data from mykavo.app, and sends the actions you take (for example "approve this change" or "run a scan"). 91 102 * **Screenshots** of your pages are loaded by the administrator's browser directly from mykavo.app, using links that expire after 30 minutes. 103 * **Once a day, while connected (WP-Cron):** the AI crawler totals for the last 14 days - for each day and crawler name, the number of visits, how many got an error, and the paths of the 10 most-visited pages. No IP addresses or visitor data. You can switch this off on the AI crawlers screen. 92 104 * **When you disconnect:** your site tells mykavo.app to revoke its key. 93 105 94 Nothing is sent when visitors view your site, and nothing about your visitors is ever sent. MyKavo scans your public pages from its own servers, the same way a visitor would.106 Nothing is sent when visitors view your site, and nothing about your human visitors is ever recorded or sent. MyKavo scans your public pages from its own servers, the same way a visitor would. 95 107 96 108 * Terms of service: https://mykavo.app/terms … … 110 122 = Will this slow down my website? = 111 123 112 No. The plugin does nothing on the pages your visitors load: no scripts, styles, queries or remote requests. It only runs inside wp-admin, on its own screens. The scanning happens on MyKavo's servers. 124 No. The plugin adds no scripts, styles, queries or remote requests to the pages your visitors load. The only public-side work is a text match on the User-Agent header; when the visitor is an AI crawler, one counter is updated after the page is built. Everything else runs inside wp-admin, and the scanning happens on MyKavo's servers. 125 126 = Why does it count AI crawlers? = 127 128 AI search is a growing way people find websites. If ChatGPT, Claude or Perplexity never read your pages, they cannot cite them. The AI crawlers screen shows which ones do, which pages they read, and which pages give them errors. It records crawler names only - never IP addresses or anything about people - and you can switch it off. 113 129 114 130 = Do I need a MyKavo account? = … … 161 177 162 178 == Changelog == 179 180 = 1.2.0 = 181 * Redesigned MyKavo screen: a new header with your site and its status, tabs with icons, and one status panel that brings together the verdict, open changes by severity and your site's uptime, response time, SSL and pages. 182 * Click a severity in the status panel to jump straight to those changes. 183 * Cleaner change lists that line up on phones, clearer before-and-after values, and a Pages list that shows each address once. 184 185 = 1.1.0 = 186 * New: AI crawlers screen - which AI systems (ChatGPT, Claude, Perplexity and more) read your site, which pages they read most, visits per day, and errors served to them. Also shown in your MyKavo dashboard. 187 * Crawler names only: no IP addresses, cookies or visitor data. Can be switched off. 163 188 164 189 = 1.0.0 = … … 177 202 == Upgrade Notice == 178 203 204 = 1.2.0 = 205 A redesigned, easier-to-read MyKavo screen. 206 207 = 1.1.0 = 208 See which AI systems read your site: the new AI crawlers screen. 209 179 210 = 1.0.0 = 180 211 First release. -
mykavo/trunk/uninstall.php
r3712737 r3721983 8 8 defined( 'WP_UNINSTALL_PLUGIN' ) || exit; 9 9 10 require_once __DIR__ . '/includes/class-mykavo-bots.php'; 11 MyKavo_Bots::uninstall(); 12 10 13 delete_option( 'mykavo_connection' ); 11 14 delete_option( 'mykavo_updates' );
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)