Plugin Directory

Changeset 3721983


Ignore:
Timestamp:
09/30/2026 08:06:22 PM (5 weeks ago)
Author:
dakshu007
Message:

Release 1.2.0: redesigned screen and AI crawlers

Location:
mykavo/trunk
Files:
1 added
8 edited

Legend:

Unmodified
Added
Removed
  • mykavo/trunk/assets/app.css

    r3712737 r3721983  
    1010
    1111.mk {
    12         --mk-ink: #151515;
     12        --mk-ink: #14161a;
    1313        --mk-ink-2: #4a4d55;
    1414        --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;
    1720        --mk-card: #ffffff;
    1821        --mk-paper: #fbfaf3;
    … …  
    3134        --mk-info: #5f636c;
    3235        --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;
    3641
    3742        color: var(--mk-ink);
    3843        font-size: 14px;
    3944        line-height: 1.5;
    40         max-width: 1180px;
     45        font-feature-settings: "cv11", "ss01";
     46        -webkit-font-smoothing: antialiased;
     47        max-width: 1200px;
    4148        padding: 20px 0 48px;
    4249}
    … …  
    117124}
    118125
    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}
    120138
    121139.mk-top {
    … …  
    127145}
    128146
     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
    129158.mk-brand {
    130159        display: inline-flex;
    … …  
    132161        gap: 9px;
    133162        font-size: 19px;
    134         font-weight: 650;
    135         letter-spacing: -0.01em;
     163        font-weight: 700;
     164        letter-spacing: -0.02em;
    136165        color: var(--mk-ink);
     166}
     167
     168.mk-shell .mk-brand {
     169        color: #fff;
    137170}
    138171
    … …  
    142175        color: var(--mk-gold);
    143176        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%));
    144181}
    145182
    … …  
    157194}
    158195
     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
    159202.mk-site b {
    160203        font-weight: 600;
    … …  
    162205}
    163206
     207.mk-shell .mk-site b {
     208        color: #fff;
     209}
     210
    164211.mk-site .mk-dot {
    165212        flex-shrink: 0;
    … …  
    173220}
    174221
     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
    175234/* ------------------------------------------------------------------ tabs -- */
    176235
    177236.mk-tabs {
    178237        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;
    184240        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;
    188246}
    189247
    190248.mk-tab {
     249        position: relative;
    191250        display: inline-flex;
    192251        align-items: center;
    193         gap: 7px;
    194         height: 34px;
    195         padding: 0 16px;
     252        gap: 8px;
     253        height: 48px;
     254        padding: 0 12px;
    196255        border: 0;
    197         border-radius: 999px;
    198256        background: transparent;
    199         color: var(--mk-ink-2);
     257        color: var(--mk-ink-3);
    200258        font-size: 13.5px;
    201         font-weight: 550;
     259        font-weight: 600;
    202260        white-space: nowrap;
    203261        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;
    205282}
    206283
    … …  
    210287
    211288.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;
    214299}
    215300
    … …  
    219304        padding: 0 6px;
    220305        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;
    224309        font-weight: 700;
    225310        line-height: 20px;
    226311        text-align: center;
     312        font-variant-numeric: tabular-nums;
    227313}
    228314
    … …  
    337423        justify-content: space-between;
    338424        gap: 12px;
    339         padding: 16px 22px;
     425        min-height: 56px;
     426        padding: 12px 22px;
    340427        border-bottom: 1px solid var(--mk-line-2);
    341428}
    342429
    343430.mk-card-title {
     431        display: inline-flex;
     432        align-items: center;
    344433        font-size: 14.5px;
    345         font-weight: 650;
    346         letter-spacing: -0.005em;
     434        font-weight: 700;
     435        letter-spacing: -0.01em;
    347436}
    348437
    … …  
    385474/* ------------------------------------------------------------------ hero -- */
    386475
     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. */
    387479.mk-hero {
    388480        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;
    395481        overflow: hidden;
     482        padding: 0;
    396483}
    397484
    … …  
    399486        content: "";
    400487        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;
    404505}
    405506
    … …  
    416517        place-items: center;
    417518        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);
    423530}
    424531
    … …  
    429536
    430537.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);
    433543        font-size: 11px;
    434544        font-weight: 600;
    … …  
    438548}
    439549
     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
    440581.mk-hero h2 {
    441         font-size: 24px;
    442         font-weight: 700;
    443         line-height: 1.2;
    444         letter-spacing: -0.02em;
     582        font-size: 26px;
     583        font-weight: 750;
     584        line-height: 1.18;
     585        letter-spacing: -0.025em;
    445586}
    446587
    … …  
    452593
    453594.mk-hero-side {
     595        position: relative;
    454596        display: flex;
    455597        flex-direction: column;
    456598        align-items: flex-end;
    457         gap: 8px;
     599        gap: 10px;
    458600}
    459601
    … …  
    502644        height: 100%;
    503645        border-radius: 999px;
    504         background: var(--mk-ink);
     646        background: linear-gradient(90deg, var(--mk-ink), #3a3d44);
    505647        transition: width 0.4s ease;
    506648}
    … …  
    521663}
    522664
    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 -- */
    524752
    525753.mk-stats {
    … …  
    529757}
    530758
    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 {
    532777        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);
    533782}
    534783
    … …  
    539788        color: var(--mk-ink-3);
    540789        font-size: 12px;
    541         font-weight: 550;
     790        font-weight: 600;
    542791}
    543792
    … …  
    549798.mk-stat-value {
    550799        margin-top: 6px !important;
    551         font-size: 22px;
    552         font-weight: 700;
    553         letter-spacing: -0.02em;
     800        font-size: 24px;
     801        font-weight: 750;
     802        letter-spacing: -0.025em;
    554803        font-variant-numeric: tabular-nums;
    555804}
    556805
    557806.mk-stat-value small {
    558         margin-left: 2px;
     807        margin-left: 3px;
    559808        color: var(--mk-ink-3);
    560809        font-size: 13px;
    561         font-weight: 550;
     810        font-weight: 600;
     811        letter-spacing: 0;
    562812}
    563813
    … …  
    615865        background: var(--mk-info-soft);
    616866        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;
    643867}
    644868
    … …  
    695919}
    696920
     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
    697938.mk-row:hover {
    698939        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);
    699959}
    700960
    … …  
    7471007        flex-shrink: 0;
    7481008        color: #b6b4aa;
     1009        transition: color 0.15s ease, transform 0.15s ease;
    7491010}
    7501011
    … …  
    9351196        padding: 22px 24px 18px;
    9361197        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);
    9381201}
    9391202
    9401203.mk-drawer-head h2 {
    941         font-size: 19px;
    942         font-weight: 700;
    943         line-height: 1.3;
    944         letter-spacing: -0.015em;
     1204        font-size: 21px;
     1205        font-weight: 750;
     1206        line-height: 1.25;
     1207        letter-spacing: -0.02em;
    9451208}
    9461209
    … …  
    9601223        border-top: 1px solid var(--mk-line);
    9611224        background: var(--mk-card);
     1225        box-shadow: 0 -10px 24px -18px rgb(20 22 26 / 30%);
    9621226}
    9631227
    … …  
    9731237        background: var(--mk-card);
    9741238        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);
    9751245}
    9761246
    … …  
    10101280
    10111281.mk-value {
    1012         padding: 12px 14px;
     1282        position: relative;
     1283        padding: 12px 14px 12px 17px;
     1284        overflow: hidden;
    10131285        border-radius: var(--mk-radius-sm);
    10141286        border: 1px solid var(--mk-line);
    10151287        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);
    10161303}
    10171304
    … …  
    15051792        }
    15061793
    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;
    15091866        }
    15101867
    … …  
    15351892        .mk-row-side {
    15361893                display: none;
     1894                font-size: 12px;
    15371895        }
    15381896
    … …  
    15521910
    15531911@media (prefers-reduced-motion: reduce) {
     1912        .mk-pulse::after {
     1913                display: none;
     1914        }
     1915
    15541916        .mk *,
    15551917        .mk *::before,
    … …  
    15761938        width: 16px;
    15771939        height: 16px;
    1578         margin-right: 7px;
     1940        flex-shrink: 0;
     1941        margin-right: 8px;
    15791942        vertical-align: -3px;
    1580         color: var(--mk-good);
     1943        color: var(--mk-ink-3);
    15811944}
    15821945
    … …  
    20012364        font-size: 12px;
    20022365}
     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  
    3939                updates: null,
    4040                updatesError: null,
     41                bots: null,
     42                botsError: null,
    4143                changesScan: null,
    4244                addUrl: '',
    … …  
    203205                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"/>',
    204206                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"/>',
    205208                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"/>',
    206209        };
    … …  
    384387                        state.updates = data;
    385388                        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' ) );
    386410                        render();
    387411                } );
    … …  
    439463                } else if ( state.tab === 'pages' && ! state.pages ) {
    440464                        loadPages();
     465                } else if ( state.tab === 'bots' ) {
     466                        loadBots();
    441467                } else if ( state.tab === 'updates' ) {
    442468                        if ( ! state.updates ) {
    … …  
    677703        }
    678704
     705        var TAB_ICONS = {
     706                overview: 'gauge',
     707                changes: 'eye',
     708                updates: 'shield',
     709                bots: 'bot',
     710                scans: 'clock',
     711                pages: 'file',
     712        };
     713
    679714        function tabs() {
    680715                var open = state.overview ? state.overview.stats.openChanges : 0;
    … …  
    683718                        [ 'changes', __( 'Changes', 'mykavo' ), open > 0 ? '<span class="mk-count">' + esc( open > 99 ? '99+' : open ) + '</span>' : '' ],
    684719                        [ 'updates', __( 'Safe Updates', 'mykavo' ), '' ],
     720                        [ 'bots', __( 'AI crawlers', 'mykavo' ), '' ],
    685721                        [ 'scans', __( 'Scans', 'mykavo' ), '' ],
    686722                        [ 'pages', __( 'Pages', 'mykavo' ), '' ],
    … …  
    690726                        list.map( function ( t ) {
    691727                                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>';
    693729                        } ).join( '' ) +
    694730                        '</div>'
    … …  
    786822                return (
    787823                        '<section class="mk-card mk-hero mk-tone-' + tone + '" aria-live="polite">' +
     824                        '<div class="mk-hero-top">' +
    788825                        '<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>' +
    790827                        ( sub ? '<p class="mk-hero-sub">' + esc( sub ) + '</p>' : '' ) + extra + '</div></div>' +
    791828                        '<div class="mk-hero-side"><div class="mk-hero-actions">' + reviewBtn + scanBtn + '</div>' +
    792829                        ( ! disabled || ! cap.manualScanBlockedReason ? '' : '<span class="mk-hero-meta">' + esc( cap.manualScanBlockedReason ) + '</span>' ) +
    793830                        ( 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>'
    795861                );
    796862        }
    … …  
    798864        function stat( ic, label, value, unit, foot ) {
    799865                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>' +
    801867                        '<p class="mk-stat-value">' + esc( value ) + ( unit ? '<small>' + esc( unit ) + '</small>' : '' ) + '</p>' +
    802868                        ( foot ? '<p class="mk-stat-foot">' + esc( foot ) + '</p>' : '' ) + '</div>'
    … …  
    810876                };
    811877                return (
    812                         '<div class="mk-stats">' +
     878                        '<div class="mk-stats mk-hero-metrics">' +
    813879                        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' ) ) +
    814880                        stat( 'gauge', __( 'Response time', 'mykavo' ), typeof h.avgResponseMs24h === 'number' ? Math.round( h.avgResponseMs24h ) : '-', typeof h.avgResponseMs24h === 'number' ? 'ms' : '', __( 'Average, 24 hours', 'mykavo' ) ) +
    … …  
    844910                        return '<div class="mk-grid">' + loadingBlock( 150 ) + loadingBlock( 96 ) + loadingBlock( 260 ) + '</div>';
    845911                }
    846                 var by = o.stats.bySeverity;
    847912                var top = o.topChanges || [];
    848913
    849914                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>' +
    851916                        ( 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>' : '' ) +
    852917                        '</div>' +
    … …  
    856921                        '</section>';
    857922
    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 
    866923                var scans = o.recentScans || [];
    867924                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>' +
    869926                        '<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>' +
    870927                        ( scans.length
    … …  
    893950                        '<div class="mk-grid">' +
    894951                        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>' +
    897953                        '</div>'
    898954                );
    … …  
    10021058                                        '<li><a class="mk-row" href="' + esc( url ) + '" target="_blank" rel="noopener noreferrer">' +
    10031059                                        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>' : '' ) +
    10061062                                        ( p.baselineVersion ? '<span>' + esc( sprintf( /* translators: %d: baseline version. */ __( 'Baseline v%d', 'mykavo' ), p.baselineVersion ) ) + '</span>' : '<span>' + esc( __( 'No baseline yet', 'mykavo' ) ) + '</span>' ) +
    10071063                                        ( p.enabled ? '' : '<span class="mk-status">' + esc( __( 'Paused', 'mykavo' ) ) + '</span>' ) +
    … …  
    12831339        }
    12841340
     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
    12851492        function render() {
    12861493                var focusKey = document.activeElement && document.activeElement.getAttribute ? document.activeElement.getAttribute( 'data-key' ) : null;
    … …  
    12931500                var view = state.tab === 'changes'
    12941501                        ? changesView()
     1502                        : state.tab === 'bots'
     1503                                ? botsView()
    12951504                        : state.tab === 'updates'
    12961505                                ? updatesView()
    … …  
    13001509                                                ? pagesView()
    13011510                                                : 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();
    13031512
    13041513                if ( focusKey ) {
    … …  
    15711780                                loadScans();
    15721781                                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;
    15731797                        case 'toggle-updates':
    15741798                                setUpdateChecks( ! ( state.updates && state.updates.enabled ) );
    … …  
    16671891                loadOverview( false );
    16681892                loadUpdates();
     1893                loadBots();
    16691894                if ( cfg.woo && cfg.woo.pages && cfg.woo.pages.length ) {
    16701895                        loadPages();
  • mykavo/trunk/includes/class-mykavo-connection.php

    r3712737 r3721983  
    6464                }
    6565                self::flush_cache();
     66                require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php';
     67                MyKavo_Bots::ensure_schedule();
    6668        }
    6769
    … …  
    7577                self::flush_cache();
    7678                delete_transient( self::SUMMARY_KEY );
     79                require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php';
     80                MyKavo_Bots::unschedule();
    7781        }
    7882
  • mykavo/trunk/includes/class-mykavo-integrations.php

    r3712737 r3721983  
    300300                        return;
    301301                }
    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>' .
    303303                        '<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>';
    304304                wp_add_privacy_policy_content( 'MyKavo', wp_kses_post( $content ) );
  • mykavo/trunk/includes/class-mykavo-rest.php

    r3712737 r3721983  
    190190                register_rest_route(
    191191                        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,
    192215                        '/disconnect',
    193216                        array(
    … …  
    376399                MyKavo_Updates::set_enabled( (bool) $request->get_param( 'enabled' ) );
    377400                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();
    378421        }
    379422
  • mykavo/trunk/mykavo.php

    r3712737 r3721983  
    44 * Plugin URI:        https://mykavo.app/wordpress-plugin
    55 * 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.0
     6 * Version:           1.2.0
    77 * Requires at least: 6.2
    88 * Requires PHP:      7.4
    … …  
    1818defined( 'ABSPATH' ) || exit;
    1919
    20 define( 'MYKAVO_VERSION', '1.0.0' );
     20define( 'MYKAVO_VERSION', '1.2.0' );
    2121define( 'MYKAVO_FILE', __FILE__ );
    2222define( 'MYKAVO_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    4242 * changes. Nothing is loaded until one of those actually runs. Everything else
    4343 * 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.
    4550 */
    4651if ( is_admin() ) {
    … …  
    5055        require_once MYKAVO_DIR . 'includes/class-mykavo-updates.php';
    5156        require_once MYKAVO_DIR . 'includes/class-mykavo-integrations.php';
     57        require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php';
    5258        MyKavo_Admin::init();
     59        add_action(
     60                'admin_init',
     61                static function () {
     62                        MyKavo_Bots::install();
     63                        MyKavo_Bots::ensure_schedule();
     64                }
     65        );
    5366        MyKavo_Integrations::init();
    5467}
    … …  
    105118);
    106119
     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 */
     127if ( ! 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}
     133add_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
    107141if ( defined( 'WP_CLI' ) && WP_CLI ) {
    108142        require_once MYKAVO_DIR . 'includes/class-mykavo-connection.php';
    … …  
    117151        static function () {
    118152                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
     159register_deactivation_hook(
     160        __FILE__,
     161        static function () {
     162                require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php';
     163                MyKavo_Bots::unschedule();
    119164        }
    120165);
    … …  
    127172                require_once MYKAVO_DIR . 'includes/class-mykavo-updates.php';
    128173                require_once MYKAVO_DIR . 'includes/class-mykavo-integrations.php';
     174                require_once MYKAVO_DIR . 'includes/class-mykavo-bots.php';
    129175                require_once MYKAVO_DIR . 'includes/class-mykavo-rest.php';
    130176                MyKavo_Rest::register_routes();
  • mykavo/trunk/readme.txt

    r3712737 r3721983  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2626* **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."
    2727* 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
     31ChatGPT, 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
     37Crawlers 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.
    2838
    2939= Built for WooCommerce stores =
    … …  
    6171Many plugins get deleted because they make a site slower. MyKavo is built so that cannot happen:
    6272
    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.
    6475* **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 scanning runs 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.
    6677* **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.
    6778* **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.
    … …  
    90101* **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").
    91102* **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.
    92104* **When you disconnect:** your site tells mykavo.app to revoke its key.
    93105
    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.
     106Nothing 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.
    95107
    96108* Terms of service: https://mykavo.app/terms
    … …  
    110122= Will this slow down my website? =
    111123
    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.
     124No. 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
     128AI 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.
    113129
    114130= Do I need a MyKavo account? =
    … …  
    161177
    162178== 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.
    163188
    164189= 1.0.0 =
    … …  
    177202== Upgrade Notice ==
    178203
     204= 1.2.0 =
     205A redesigned, easier-to-read MyKavo screen.
     206
     207= 1.1.0 =
     208See which AI systems read your site: the new AI crawlers screen.
     209
    179210= 1.0.0 =
    180211First release.
  • mykavo/trunk/uninstall.php

    r3712737 r3721983  
    88defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
    99
     10require_once __DIR__ . '/includes/class-mykavo-bots.php';
     11MyKavo_Bots::uninstall();
     12
    1013delete_option( 'mykavo_connection' );
    1114delete_option( 'mykavo_updates' );
Note: See TracChangeset for help on using the changeset viewer.