Plugin Directory

Changeset 3739629


Ignore:
Timestamp:
10/11/2026 02:52:48 PM (5 weeks ago)
Author:
enetrato
Message:

Release VeloIMG 0.9.7

Location:
veloimg
Files:
10 added
26 edited

Legend:

Unmodified
Added
Removed
  • veloimg/trunk/assets/css/admin-base.css

    r3728637 r3739629  
    11/*
    2  * VeloIMG Admin UI - WordPress style base skin.
     2 * VeloIMG Admin UI - approved warm WordPress-native skin.
    33 * This is the single admin UI stylesheet used by the plugin.
    44 */
    55
    66.wrap.velowp{
    7         --vw-border: #c3c4c7;
    8         --vw-border-soft: #dcdcde;
    9         --vw-surface: #fff;
    10         --vw-surface-muted: #f6f7f7;
    11         --vw-text: #1d2327;
    12         --vw-muted: #646970;
     7        --vw-page: #f3f0eb;
     8        --vw-border: #ded8d0;
     9        --vw-border-soft: #e6e0d8;
     10        --vw-control-border: #aaa39a;
     11        --vw-surface: #fffdfa;
     12        --vw-surface-muted: #f7f4ef;
     13        --vw-accent: #a65338;
     14        --vw-accent-bg: #f5efe8;
     15        --vw-accent-bg-open: #eee5dc;
     16        --vw-text: #302d29;
     17        --vw-muted: #615950;
    1318        --vw-primary: #2271b1;
    1419        --vw-primary-dark: #135e96;
    15         --vw-success: #00a32a;
    16         --vw-success-bg: #edfaef;
    17         --vw-warning: #dba617;
    18         --vw-warning-bg: #fcf9e8;
    19         --vw-danger: #d63638;
    20         --vw-danger-bg: #fcf0f1;
    21         --vw-info-bg: #edf6fc;
     20        --vw-success: #309454;
     21        --vw-success-text: #206344;
     22        --vw-success-border: #c3dacb;
     23        --vw-success-bg: #edf5ef;
     24        --vw-warning: #b77a1c;
     25        --vw-warning-text: #755016;
     26        --vw-warning-border: #e6d4af;
     27        --vw-warning-bg: #fcf3de;
     28        --vw-danger: #b53f42;
     29        --vw-danger-text: #853638;
     30        --vw-danger-border: #e9c7c8;
     31        --vw-danger-bg: #fbefef;
     32        --vw-info-border: #cadce7;
     33        --vw-info-bg: #edf4f9;
     34        --vw-radius: 4px;
     35        --vw-panel-radius: 6px;
    2236        color: var(--vw-text);
     37        font-size: 14px;
     38        line-height: 1.6;
    2339        max-width: 1280px;
     40}
     41
     42.wrap.velowp p,
     43.wrap.velowp .description{
     44        font-size: 14px;
     45        line-height: 1.6;
    2446}
    2547
    … …  
    3456}
    3557
     58.velowp-concurrency-warning{
     59        position: relative;
     60        margin: 12px 0;
     61        padding: 12px 44px 12px 16px;
     62        border-radius: var(--vw-radius);
     63        background: var(--vw-warning-bg);
     64}
     65.velowp-concurrency-dismiss{
     66        position: absolute;
     67        top: 6px;
     68        right: 6px;
     69        width: 32px;
     70        height: 32px;
     71        padding: 6px;
     72        border: 0;
     73        border-radius: var(--vw-radius);
     74        background: transparent;
     75        color: var(--vw-warning-text);
     76        cursor: pointer;
     77}
     78.velowp-concurrency-dismiss:hover{
     79        background: var(--vw-warning-border);
     80}
     81.velowp-concurrency-dismiss:focus-visible{
     82        outline: 2px solid var(--vw-primary);
     83        outline-offset: 1px;
     84}
     85.velowp-concurrency-warning[hidden]{
     86        display: none;
     87}
     88
     89/* WordPress marks JS-enabled pages before footer scripts load. Keep the
     90   server-selected panel stable on the first paint; no-JS retains all sections. */
     91body.js .velowp-shell:not(.velowp-shell--js) .velowp-panel,
    3692.velowp-shell--js .velowp-panel{
    3793        display: none;
    3894}
    3995
     96body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-panel[data-panel="mass"],
     97body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-panel[data-panel="setup"],
     98body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-panel[data-panel="mass"],
     99body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-panel[data-panel="setup"],
    40100.velowp-shell--js .velowp-panel.is-active{
    41101        display: block;
    … …  
    47107        justify-content: space-between;
    48108        gap: 16px;
    49         margin: 16px 0 0;
    50         padding: 0 0 14px;
    51         border-bottom: 1px solid var(--vw-border);
     109        margin: 24px 0 0;
    52110}
    53111
    … …  
    72130.wrap.velowp img.velowp-topbar__logo--wide,
    73131.velowp-topbar__logo--wide{
    74         width: min(110px, 45vw);
     132        width: min(148px, 45vw);
    75133        height: auto;
    76         max-width: min(110px, 45vw);
    77         max-height: 30px;
     134        max-width: min(148px, 45vw);
     135        max-height: 40px;
    78136}
    79137
    … …  
    83141        justify-content: space-between;
    84142        gap: 24px;
    85         max-width: 1120px;
    86143        margin: 24px 0;
    87144}
    … …  
    99156
    100157.velowp-home-logo__image--wide{
    101         max-width: 160px;
     158        max-width: 220px;
    102159}
    103160
    … …  
    124181        padding: 3px 8px;
    125182        border: 1px solid var(--vw-border-soft);
     183        border-radius: var(--vw-radius);
    126184        background: var(--vw-surface-muted);
    127185        color: var(--vw-text);
    128186        font-size: 12px;
    129187        font-weight: 600;
    130         line-height: 1.7;
     188        line-height: 1.6;
    131189}
    132190
    133191.velowp-pill--ok{
    134         border-color: #b8e6c1;
     192        border-color: var(--vw-success-border);
    135193        background: var(--vw-success-bg);
    136         color: #0a4b1f;
     194        color: var(--vw-success-text);
    137195}
    138196
    139197.velowp-pill--bad{
    140         border-color: #f0c5c7;
     198        border-color: var(--vw-danger-border);
    141199        background: var(--vw-danger-bg);
    142         color: #8a2424;
     200        color: var(--vw-danger-text);
    143201}
    144202
    … …  
    147205        flex-wrap: wrap;
    148206        gap: 0;
    149         margin: 18px 0 0;
    150         padding-top: 9px;
     207        margin: 24px 0 0;
    151208        border-bottom: 1px solid var(--vw-border);
    152209}
    … …  
    156213        appearance: none;
    157214        border: 1px solid var(--vw-border);
    158         background: #f0f0f1;
    159         color: #2c3338;
     215        background: var(--vw-accent-bg);
     216        color: var(--vw-text);
    160217        cursor: pointer;
    161218        font-size: 14px;
     219        font-family: inherit;
    162220        line-height: 1.71428571;
    163221        text-decoration: none;
    … …  
    166224.velowp-nav__btn{
    167225        margin: 0 4px -1px 0;
    168         padding: 5px 10px;
     226        padding: 8px 14px;
     227        min-height: 40px;
     228        border-radius: var(--vw-radius) var(--vw-radius) 0 0;
    169229        border-bottom: 1px solid var(--vw-border);
    170230}
    171231
     232body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-nav__btn[data-panel="mass"],
     233body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-nav__btn[data-panel="setup"],
     234body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-nav__btn[data-panel="mass"],
     235body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-nav__btn[data-panel="setup"],
    172236.velowp-nav__btn.is-active{
    173237        border-bottom-color: var(--vw-surface);
    174238        background: var(--vw-surface);
    175         color: #000;
     239        color: var(--vw-primary);
     240        font-weight: 600;
     241        box-shadow: inset 0 2px 0 var(--vw-primary);
     242}
     243
     244.velowp-nav__btn:hover,
     245.velowp-topbtn:hover{
     246        color: var(--vw-primary-dark);
     247}
     248
     249.velowp-nav__btn:focus-visible,
     250.velowp-topbtn:focus-visible{
     251        outline: 2px solid var(--vw-primary);
     252        outline-offset: 2px;
    176253}
    177254
    178255.velowp-topbtn{
    179         padding: 4px 10px;
     256        padding: 6px 12px;
     257        border-radius: var(--vw-radius);
    180258}
    181259
    … …  
    183261.velowp-quick-card,
    184262.velowp-mass-card{
     263        min-width: 0;
    185264        margin: 16px 0 0;
    186         padding: 16px;
     265        padding: 24px;
    187266        border: 1px solid var(--vw-border);
     267        border-radius: var(--vw-panel-radius);
    188268        background: var(--vw-surface);
    189         box-shadow: 0 1px 1px rgba(0,0,0,.04);
     269        box-shadow: none;
    190270}
    191271
    192272.velowp-card--glass{
    193273        background: var(--vw-surface);
     274}
     275
     276.velowp-home-accordion{
     277        padding: 0;
     278}
     279
     280.velowp-home-accordion > summary{
     281        display: flex;
     282        align-items: center;
     283        justify-content: space-between;
     284        gap: 12px;
     285        padding: 16px 20px;
     286        cursor: pointer;
     287        list-style: none;
     288}
     289
     290.velowp-home-accordion > summary:hover{
     291        background: var(--vw-surface-muted);
     292}
     293
     294.velowp-home-accordion[open] > summary{
     295        border-bottom: 1px solid var(--vw-border-soft);
     296}
     297
     298.velowp-home-accordion > summary::-webkit-details-marker{
     299        display: none;
     300}
     301
     302.velowp-home-accordion > summary .velowp-quick-card__title{
     303        margin: 0;
     304}
     305
     306.velowp-home-accordion > summary::after{
     307        content: '';
     308        flex: 0 0 8px;
     309        height: 8px;
     310        border-right: 2px solid currentColor;
     311        border-bottom: 2px solid currentColor;
     312        transform: rotate(45deg);
     313}
     314
     315.velowp-home-accordion[open] > summary::after{
     316        transform: rotate(225deg);
     317}
     318
     319.velowp-home-accordion > summary:focus-visible{
     320        outline: 2px solid var(--vw-primary);
     321        outline-offset: -2px;
     322}
     323
     324.velowp-home-accordion__body{
     325        padding: 20px;
    194326}
    195327
    … …  
    204336
    205337.velowp-mass-grid--top{
    206         grid-template-columns: minmax(0, 1.35fr) minmax(280px, .65fr);
     338        grid-template-columns: minmax(0, 1.8fr) minmax(280px, 1fr);
     339}
     340
     341.velowp-panel[data-panel="setup"] > .velowp-grid{
     342        grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
    207343}
    208344
    209345.velowp-mass-grid--bottom{
    210346        grid-template-columns: 1fr;
     347}
     348
     349.velowp-mass-main-column{
     350        min-width: 0;
     351}
     352
     353.velowp-webp-retry-controls{
     354        display: flex;
     355        align-items: flex-end;
     356        gap: 16px;
     357        flex-wrap: wrap;
     358}
     359
     360.velowp-webp-retry-controls label{
     361        display: block;
     362        margin-bottom: 8px;
     363        font-weight: 600;
     364}
     365
     366.velowp-webp-retry-quality{
     367        flex: 1 1 200px;
     368}
     369
     370.velowp-webp-retry-quality input{
     371        width: 100%;
     372        margin: 0;
     373}
     374
     375.velowp-webp-retry-engine select{
     376        min-width: 140px;
     377        max-width: 100%;
    211378}
    212379
    … …  
    220387
    221388.velowp-h2{
    222         font-size: 20px;
     389        font-size: 22px;
    223390        font-weight: 600;
    224391}
    … …  
    227394.velowp-h3,
    228395.velowp-quick-card__title{
    229         font-size: 16px;
     396        font-size: 18px;
    230397        font-weight: 600;
    231398}
    … …  
    256423.wrap.velowp input[type="url"]{
    257424        max-width: 100%;
     425        min-height: 38px;
     426        padding: 6px 10px;
     427        border-color: var(--vw-control-border);
     428        border-radius: var(--vw-radius);
     429        font-size: 14px;
     430        line-height: 1.6;
     431}
     432
     433.wrap.velowp select{
     434        padding-right: 30px;
     435}
     436
     437.wrap.velowp input[type="range"],
     438.wrap.velowp input[type="checkbox"],
     439.wrap.velowp input[type="radio"]{
     440        accent-color: var(--vw-primary);
     441}
     442
     443.wrap.velowp input[type="checkbox"],
     444.wrap.velowp input[type="radio"]{
     445        flex-shrink: 0;
    258446}
    259447
    … …  
    263451
    264452.wrap.velowp .velowp-home-card{
    265         max-width: 1120px;
     453        max-width: none;
     454        padding: 24px;
     455        border-color: var(--vw-border);
     456        border-radius: var(--vw-panel-radius);
     457        background: var(--vw-surface);
     458        box-shadow: none;
     459}
     460
     461.velowp-home-card > h2{
     462        margin: 0 0 12px;
     463        font-size: 22px;
     464        line-height: 1.3;
    266465}
    267466
    268467.velowp-home-status-grid{
    269468        display: grid;
    270         grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    271         gap: 12px;
    272         margin: 18px 0;
     469        grid-template-columns: repeat(4, minmax(0, 1fr));
     470        gap: 16px;
     471        margin: 24px 0 20px;
    273472}
    274473
    … …  
    276475        min-width: 0;
    277476        min-height: 150px;
    278         padding: 14px;
     477        padding: 16px;
    279478        border: 1px solid var(--vw-border);
     479        border-radius: var(--vw-radius);
    280480        background: var(--vw-surface);
    281         box-shadow: 0 1px 1px rgba(0,0,0,.04);
    282 }
    283 
    284 .velowp-home-status-card.is-success{
    285         border-color: #b8e6c1;
    286         background: linear-gradient(0deg, var(--vw-success-bg), var(--vw-surface) 58%);
    287 }
    288 
    289 .velowp-home-status-card.is-warning{
    290         border-color: #ead483;
    291         background: linear-gradient(0deg, var(--vw-warning-bg), var(--vw-surface) 58%);
    292 }
    293 
    294 .velowp-home-status-card.is-error{
    295         border-color: #f0c5c7;
    296         background: linear-gradient(0deg, var(--vw-danger-bg), var(--vw-surface) 58%);
     481        box-shadow: none;
    297482}
    298483
    … …  
    307492
    308493.velowp-home-status-card__head strong{
    309         flex: 1 1 120px;
     494        flex: 1 1 100px;
    310495        min-width: 0;
    311         font-size: 14px;
     496        font-size: 16px;
    312497        line-height: 1.35;
    313498}
    … …  
    334519
    335520.velowp-home-strategy-value.is-success{
    336         border-color: #b8e6c1;
     521        border-color: var(--vw-success-border);
    337522        background: var(--vw-success-bg);
    338         color: #0a4b1f;
     523        color: var(--vw-success-text);
    339524}
    340525
    341526.velowp-home-strategy-value.is-warning{
    342         border-color: #ead483;
     527        border-color: var(--vw-warning-border);
    343528        background: var(--vw-warning-bg);
    344         color: #5c4300;
     529        color: var(--vw-warning-text);
    345530}
    346531
    347532.velowp-home-strategy-value.is-error{
    348         border-color: #f0c5c7;
     533        border-color: var(--vw-danger-border);
    349534        background: var(--vw-danger-bg);
    350         color: #8a2424;
     535        color: var(--vw-danger-text);
     536}
     537
     538.wrap.velowp select.velowp-home-badge-select{
     539        appearance: auto;
     540        background-image: none;
     541        min-height: 38px;
     542        max-width: 100%;
     543        width: 100%;
     544        padding: 6px 24px 6px 10px;
     545        border-color: var(--vw-border);
     546        border-radius: var(--vw-radius);
     547        background-color: var(--vw-surface-muted);
     548        color: var(--vw-text);
     549        font-size: 14px;
     550        font-weight: 400;
     551        line-height: 1.6;
     552        cursor: pointer;
     553        text-align: left;
     554}
     555
     556.velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head{
     557        flex-direction: column;
     558        align-items: stretch;
     559}
     560
     561.velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{
     562        flex: none;
     563}
     564
     565.velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head,
     566.velowp-home-status-card--strategy .velowp-home-status-card__head{
     567        min-height: 0;
     568        margin-bottom: 10px;
     569}
     570
     571.velowp-home-generation-result{
     572        margin-top: 16px;
     573        padding-top: 12px;
     574        border-top: 1px solid var(--vw-border-soft);
     575}
     576
     577.velowp-home-generation-result [data-velowp-home-summary="saved"] strong{
     578        display: block;
     579        margin-top: 4px;
     580        color: var(--vw-text);
     581        font-size: 34px;
     582        font-weight: 600;
     583        line-height: 1.2;
     584}
     585
     586.wrap.velowp select.velowp-home-strategy-value{
     587        width: 100%;
    351588}
    352589
    … …  
    355592}
    356593
     594.velowp-diagnostics-tests{
     595        display: grid;
     596        grid-template-columns: repeat(2, minmax(0, 1fr));
     597        gap: 16px;
     598}
     599
     600.velowp-diagnostics-tests > form{
     601        min-width: 0;
     602        margin-top: 0;
     603        padding: 16px;
     604}
     605
     606.velowp-bypass-copy{
     607        display: flex;
     608        flex-wrap: wrap;
     609        align-items: center;
     610        gap: 8px;
     611}
     612
     613.velowp-bypass-copy input.regular-text{
     614        max-width: 100%;
     615        cursor: pointer;
     616}
     617
     618@media (max-width: 782px){
     619        .velowp-diagnostics-tests{ grid-template-columns: minmax(0, 1fr); }
     620}
     621
    357622.velowp-help-page{
    358         max-width: 1120px;
     623        max-width: 1280px;
    359624}
    360625
    … …  
    368633.velowp-help-grid{
    369634        display: grid;
    370         grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
     635        grid-template-columns: repeat(5, minmax(0, 1fr));
    371636        gap: 12px;
    372         margin: 18px 0;
     637        margin: 24px 0;
    373638}
    374639
    375640.velowp-help-card{
    376         padding: 14px;
     641        min-width: 0;
     642        padding: 16px;
    377643        border: 1px solid var(--vw-border);
     644        border-radius: var(--vw-radius);
    378645        background: var(--vw-surface);
    379         box-shadow: 0 1px 1px rgba(0,0,0,.04);
     646        box-shadow: none;
    380647}
    381648
    382649.velowp-help-card h2{
    383650        margin: 0 0 8px;
    384         font-size: 14px;
     651        font-size: 16px;
    385652        line-height: 1.35;
    386653}
    … …  
    392659
    393660.velowp-help-section{
    394         max-width: 1120px;
     661        max-width: none;
    395662}
    396663
    … …  
    403670.velowp-help-faq details{
    404671        border: 1px solid var(--vw-border-soft);
     672        border-radius: var(--vw-radius);
    405673        background: var(--vw-surface);
    406674}
    407675
    408676.velowp-help-faq summary{
    409         padding: 12px 14px;
     677        display: flex;
     678        align-items: center;
     679        justify-content: space-between;
     680        gap: 16px;
     681        padding: 12px 16px;
    410682        color: var(--vw-text);
    411683        font-weight: 600;
    412684        cursor: pointer;
     685        list-style: none;
     686}
     687
     688.velowp-help-faq summary::-webkit-details-marker{
     689        display: none;
     690}
     691
     692.velowp-help-faq summary::after{
     693        content: '';
     694        flex: 0 0 8px;
     695        height: 8px;
     696        border-right: 2px solid currentColor;
     697        border-bottom: 2px solid currentColor;
     698        transform: rotate(45deg);
     699}
     700
     701.velowp-help-faq details[open] summary::after{
     702        color: var(--vw-primary);
     703        transform: rotate(225deg);
     704}
     705
     706.velowp-help-faq summary:focus-visible{
     707        outline: 2px solid var(--vw-primary);
     708        outline-offset: -2px;
    413709}
    414710
    415711.velowp-help-faq details[open] summary{
    416712        border-bottom: 1px solid var(--vw-border-soft);
    417         background: var(--vw-surface-muted);
     713        background: var(--vw-surface);
    418714}
    419715
    420716.velowp-help-faq p{
    421717        margin: 0;
    422         padding: 12px 14px 14px;
     718        padding: 12px 16px 16px;
    423719        color: var(--vw-muted);
    424720}
    … …  
    428724        align-items: center;
    429725        gap: 10px;
    430         max-width: 420px;
     726        width: 100%;
    431727}
    432728
    433729.velowp-range input[type="range"]{
    434         flex: 1;
     730        flex: 1 1 auto;
     731        min-width: 0;
     732        margin: 0;
     733        height: 28px;
    435734}
    436735
    … …  
    439738        text-align: right;
    440739        font-weight: 600;
     740}
     741
     742.velowp-range-scale{
     743        display: flex;
     744        justify-content: space-between;
     745        margin-right: 46px;
     746        color: var(--vw-muted);
     747        font-size: 12px;
    441748}
    442749
    … …  
    447754        align-items: center;
    448755        gap: 8px;
     756}
     757
     758.velowp-toggle{
     759        align-items: flex-start;
     760}
     761
     762.velowp-toggle input{
     763        margin-top: 3px;
    449764}
    450765
    … …  
    459774        border-radius: 50%;
    460775        background: var(--vw-surface-muted);
    461         color: #2c3338;
     776        color: var(--vw-text);
    462777        font-size: 12px;
    463778        font-weight: 600;
    … …  
    525840}
    526841
     842.velowp-panel[data-panel="setup"] .form-table tr{
     843        display: block;
     844        padding: 20px 0;
     845        border-top: 1px solid var(--vw-border-soft);
     846}
     847
     848.velowp-panel[data-panel="setup"] .form-table th,
     849.velowp-panel[data-panel="setup"] .form-table td{
     850        display: block;
     851        width: 100%;
     852        padding: 0;
     853}
     854
     855.velowp-panel[data-panel="setup"] .form-table th{
     856        margin-bottom: 12px;
     857        font-size: 18px;
     858}
     859
     860.velowp-verdict strong{
     861        display: inline-block;
     862        margin: 0 0 12px;
     863        padding: 3px 10px;
     864        border: 1px solid var(--vw-border-soft);
     865        border-radius: var(--vw-radius);
     866}
     867
     868.velowp-verdict-ok strong{
     869        border-color: var(--vw-success-border);
     870        background: var(--vw-success-bg);
     871        color: var(--vw-success-text);
     872}
     873
     874.velowp-verdict-bad strong{
     875        border-color: var(--vw-danger-border);
     876        background: var(--vw-danger-bg);
     877        color: var(--vw-danger-text);
     878}
     879
     880.velowp-verdict .description{
     881        display: block;
     882        margin-bottom: 16px;
     883}
     884
    527885.velowp-strategy-options{
    528886        display: grid;
    … …  
    559917}
    560918
     919.velowp-diagnostics-tests .velowp-quick-inline--top{
     920        flex-direction: column;
     921        align-items: stretch;
     922}
     923
     924.velowp-diagnostics-tests .velowp-quick-pick{
     925        flex: 0 1 auto;
     926}
     927
    561928.velowp-quick-pick{
    562929        flex: 1 1 320px;
    … …  
    573940.wrap.velowp .velowp-btn-glass--primary{
    574941        min-width: 0;
     942        min-height: 38px;
     943        padding: 6px 14px;
     944}
     945
     946.wrap.velowp .button{
     947        min-height: 38px;
     948        padding: 6px 14px;
     949        border-color: var(--vw-primary);
     950        border-radius: var(--vw-radius);
     951        background: var(--vw-surface);
     952        color: var(--vw-primary);
     953        font-family: inherit;
     954        font-size: 14px;
     955        line-height: 1.6;
     956}
     957
     958.wrap.velowp .button-primary{
     959        border-color: var(--vw-primary);
     960        background: var(--vw-primary);
     961        color: #fff;
     962}
     963
     964.wrap.velowp .button-primary:hover,
     965.wrap.velowp .button-primary:focus{
     966        border-color: var(--vw-primary-dark);
     967        background: var(--vw-primary-dark);
     968}
     969
     970.wrap.velowp .button-small{
    575971        min-height: 30px;
    576         padding: 0 12px;
     972        padding: 3px 10px;
     973        font-size: 12px;
    577974}
    578975
    … …  
    584981.wrap.velowp .velowp-btn-warn{
    585982        border-color: var(--vw-warning);
    586         color: #7a5b00;
     983        color: var(--vw-warning-text);
    587984}
    588985
    589986.wrap.velowp .velowp-btn-danger{
    590         border-color: var(--vw-danger);
    591         background: var(--vw-danger);
    592         color: #fff;
     987        border-color: var(--vw-danger-border);
     988        background: var(--vw-danger-bg);
     989        color: var(--vw-danger-text);
    593990}
    594991
    595992.wrap.velowp .velowp-btn-danger-outline{
    596993        border-color: var(--vw-danger);
    597         color: #8a2424;
     994        color: var(--vw-danger-text);
    598995}
    599996
    600997.wrap.velowp .button[disabled],
    601998.wrap.velowp .button:disabled{
    602         opacity: .65;
     999        border-color: var(--vw-border) !important;
     1000        background: var(--vw-surface-muted) !important;
     1001        color: #756f65 !important;
     1002        opacity: 1;
    6031003        cursor: not-allowed;
    6041004}
    … …  
    6501050.velowp-quick-result-row{
    6511051        display: grid;
    652         grid-template-columns: 220px minmax(0, 1fr);
     1052        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    6531053        gap: 12px;
    6541054        padding-top: 10px;
    … …  
    6701070        align-items: center;
    6711071        gap: 8px;
     1072}
     1073
     1074.velowp-quick-result-value{
     1075        min-width: 0;
     1076        overflow-wrap: anywhere;
    6721077}
    6731078
    … …  
    6991104        padding: 12px;
    7001105        border: 1px solid var(--vw-border-soft);
     1106        border-radius: var(--vw-radius);
    7011107        background: var(--vw-surface-muted);
    7021108}
    … …  
    7071113
    7081114.velowp-self-test-summary.is-success{
    709         border-left: 4px solid var(--vw-success);
    7101115        background: var(--vw-success-bg);
    7111116}
    7121117
    7131118.velowp-self-test-summary.is-warning{
    714         border-left: 4px solid var(--vw-warning);
    7151119        background: var(--vw-warning-bg);
    7161120}
    7171121
    7181122.velowp-self-test-summary.is-error{
    719         border-left: 4px solid var(--vw-danger);
    7201123        background: var(--vw-danger-bg);
    7211124}
    … …  
    7331136        padding: 10px 12px;
    7341137        border: 1px solid var(--vw-border-soft);
     1138        border-radius: var(--vw-radius);
    7351139        background: var(--vw-surface);
    7361140}
    … …  
    7491153}
    7501154
    751 .velowp-home-generation-result{ border-top: 1px solid var(--vw-border, #dcdcde); margin-top: 10px; padding-top: 8px; }
    7521155.velowp-home-last-scope{ display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    7531156
    … …  
    7581161        padding: 2px 8px;
    7591162        border: 1px solid var(--vw-border-soft);
     1163        border-radius: var(--vw-radius);
    7601164        background: var(--vw-surface);
    7611165        font-size: 12px;
    … …  
    7661170.velowp-self-test-badge.is-success,
    7671171.velowp-badge--success{
    768         border-color: #b8e6c1;
     1172        border-color: var(--vw-success-border);
    7691173        background: var(--vw-success-bg);
    770         color: #0a4b1f;
     1174        color: var(--vw-success-text);
    7711175}
    7721176
    7731177.velowp-self-test-badge.is-warning,
    7741178.velowp-badge--warning{
    775         border-color: #ead9a1;
     1179        border-color: var(--vw-warning-border);
    7761180        background: var(--vw-warning-bg);
    777         color: #7a5b00;
     1181        color: var(--vw-warning-text);
    7781182}
    7791183
    7801184.velowp-self-test-badge.is-error,
    7811185.velowp-badge--danger{
    782         border-color: #f0c5c7;
     1186        border-color: var(--vw-danger-border);
    7831187        background: var(--vw-danger-bg);
    784         color: #8a2424;
     1188        color: var(--vw-danger-text);
    7851189}
    7861190
    7871191.velowp-self-test-badge.is-info,
    7881192.velowp-badge--info{
    789         border-color: #c5d9ed;
     1193        border-color: var(--vw-info-border);
    7901194        background: var(--vw-info-bg);
    791         color: #0a4b78;
     1195        color: var(--vw-primary-dark);
    7921196}
    7931197
    … …  
    8111215.velowp-audit-metrics{
    8121216        display: grid;
    813         grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    814         gap: 10px;
     1217        grid-template-columns: repeat(4, minmax(0, 1fr));
     1218        gap: 16px;
    8151219        margin-top: 12px;
    8161220}
    … …  
    8181222.velowp-audit-metric{
    8191223        display: grid;
    820         grid-template-columns: minmax(0, 1fr) auto;
     1224        grid-template-columns: minmax(0, 1fr);
    8211225        grid-template-areas:
    822                 "head value"
    823                 "description description";
     1226                "head"
     1227                "value"
     1228                "description";
    8241229        align-items: flex-start;
    8251230        gap: 8px 14px;
    8261231        min-height: 86px;
    827         padding: 12px;
     1232        min-width: 0;
     1233        padding: 16px;
    8281234        border: 1px solid var(--vw-border-soft);
    829         border-left: 4px solid var(--vw-border-soft);
     1235        border-radius: var(--vw-radius);
    8301236        background: var(--vw-surface);
    831         box-shadow: 0 1px 1px rgba(0,0,0,.03);
    832 }
    833 
    834 .velowp-audit-metric.is-success{
    835         border-left-color: var(--vw-success);
    836         background: linear-gradient(90deg, var(--vw-success-bg), var(--vw-surface) 42%);
    837 }
    838 
    839 .velowp-audit-metric.is-warning{
    840         border-left-color: var(--vw-warning);
    841         background: linear-gradient(90deg, var(--vw-warning-bg), var(--vw-surface) 42%);
    842 }
    843 
    844 .velowp-audit-metric.is-error{
    845         border-left-color: var(--vw-danger);
    846         background: linear-gradient(90deg, var(--vw-danger-bg), var(--vw-surface) 42%);
    847 }
    848 
    849 .velowp-audit-metric.is-info{
    850         border-left-color: #72aee6;
    851         background: linear-gradient(90deg, var(--vw-info-bg), var(--vw-surface) 42%);
     1237        box-shadow: none;
     1238}
     1239
     1240.velowp-audit-metrics:not(.velowp-audit-metrics--no-generated) > .velowp-audit-metric:nth-child(n+5){
     1241        grid-column: span 2;
    8521242}
    8531243
    8541244.velowp-audit-metric__head{
    8551245        display: flex;
    856         flex-wrap: wrap;
    857         align-items: center;
     1246        justify-content: space-between;
     1247        align-items: flex-start;
    8581248        gap: 6px;
    8591249        grid-area: head;
    … …  
    8621252
    8631253.velowp-audit-metric span{
    864         color: var(--vw-muted);
    865         font-size: 12px;
    866         font-weight: 600;
    867         text-transform: uppercase;
     1254        color: var(--vw-text);
     1255        font-size: 16px;
     1256        font-weight: 600;
     1257        line-height: 1.35;
    8681258}
    8691259
    … …  
    8831273
    8841274.velowp-audit-metric.is-success em{
    885         border-color: #b8e6c1;
     1275        border-color: var(--vw-success-border);
    8861276        background: var(--vw-success-bg);
    887         color: #0a4b1f;
     1277        color: var(--vw-success-text);
    8881278}
    8891279
    8901280.velowp-audit-metric.is-warning em{
    891         border-color: #ead9a1;
     1281        border-color: var(--vw-warning-border);
    8921282        background: var(--vw-warning-bg);
    893         color: #7a5b00;
     1283        color: var(--vw-warning-text);
    8941284}
    8951285
    8961286.velowp-audit-metric.is-error em{
    897         border-color: #f0c5c7;
     1287        border-color: var(--vw-danger-border);
    8981288        background: var(--vw-danger-bg);
    899         color: #8a2424;
     1289        color: var(--vw-danger-text);
    9001290}
    9011291
    9021292.velowp-audit-metric.is-info em{
    903         border-color: #c5d9ed;
     1293        border-color: var(--vw-info-border);
    9041294        background: var(--vw-info-bg);
    905         color: #0a4b78;
     1295        color: var(--vw-primary-dark);
    9061296}
    9071297
    9081298.velowp-audit-metric__value{
    9091299        grid-area: value;
    910         justify-self: end;
    911         color: var(--vw-text);
    912         font-size: 24px;
     1300        justify-self: start;
     1301        color: var(--vw-primary);
     1302        font-size: 34px;
    9131303        line-height: 1.1;
    9141304        white-space: nowrap;
    … …  
    9161306
    9171307.velowp-audit-metric.is-success .velowp-audit-metric__value{
    918         color: #0a4b1f;
     1308        color: var(--vw-primary);
    9191309}
    9201310
    9211311.velowp-audit-metric.is-warning .velowp-audit-metric__value{
    922         color: #7a5b00;
     1312        color: var(--vw-warning-text);
    9231313}
    9241314
    9251315.velowp-audit-metric.is-error .velowp-audit-metric__value{
    926         color: #8a2424;
     1316        color: var(--vw-danger-text);
    9271317}
    9281318
    9291319.velowp-audit-metric.is-info .velowp-audit-metric__value{
    930         color: #0a4b78;
     1320        color: var(--vw-primary);
    9311321}
    9321322
    … …  
    9351325        grid-area: description;
    9361326        color: var(--vw-muted);
    937         font-size: 12px;
    938         line-height: 1.35;
     1327        padding-top: 10px;
     1328        border-top: 1px solid var(--vw-border-soft);
     1329        font-size: 14px;
     1330        line-height: 1.5;
    9391331}
    9401332
    9411333.velowp-audit-metrics--no-generated{
    942         grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
     1334        grid-template-columns: repeat(2, minmax(0, 1fr));
    9431335}
    9441336
    … …  
    9601352        padding: 12px 14px;
    9611353        border: 1px solid var(--vw-border-soft);
    962         border-left: 4px solid var(--vw-warning);
    9631354        background: var(--vw-warning-bg);
    9641355}
    … …  
    10141405        align-items: center;
    10151406        gap: 6px;
    1016         padding: 5px 9px;
     1407        padding: 7px 10px;
    10171408        border: 1px solid var(--vw-border-soft);
     1409        border-radius: var(--vw-radius);
    10181410        background: var(--vw-surface-muted);
    10191411        cursor: pointer;
     1412}
     1413
     1414.velowp-seg:has(input:checked){
     1415        border-color: #72aee6;
     1416        background: var(--vw-info-bg);
     1417}
     1418
     1419.velowp-seg:has(input:focus-visible){
     1420        outline: 2px solid var(--vw-primary);
     1421        outline-offset: 2px;
     1422}
     1423
     1424.velowp-seg input{
     1425        margin: 0;
    10201426}
    10211427
    … …  
    10401446        padding: 7px 10px;
    10411447        border: 1px solid var(--vw-border-soft);
     1448        border-radius: var(--vw-radius);
    10421449        background: var(--vw-surface);
    10431450}
    … …  
    10531460
    10541461.velowp-chip--ok{
    1055         border-color: #b8e6c1;
     1462        border-color: var(--vw-success-border);
    10561463        background: var(--vw-success-bg);
    1057         color: #0a4b1f;
     1464        color: var(--vw-success-text);
    10581465}
    10591466
    10601467.velowp-chip--queue{
    1061         border-color: #c5d9ed;
     1468        border-color: var(--vw-info-border);
    10621469        background: var(--vw-info-bg);
    1063         color: #0a4b78;
     1470        color: var(--vw-primary-dark);
    10641471}
    10651472
    10661473.velowp-chip--bad{
    1067         border-color: #f0c5c7;
    1068         background: var(--vw-danger-bg);
    1069         color: #8a2424;
     1474        border-color: var(--vw-warning-border);
     1475        background: var(--vw-warning-bg);
     1476        color: var(--vw-warning-text);
    10701477}
    10711478
    … …  
    10791486.velowp-progress{
    10801487        flex: 1 1 auto;
    1081         height: 12px;
     1488        height: 14px;
    10821489        overflow: hidden;
    10831490        border: 1px solid var(--vw-border-soft);
     1491        border-radius: var(--vw-radius);
    10841492        background: var(--vw-surface-muted);
    10851493}
    … …  
    11131521.velowp-donut{
    11141522        --pct: 0;
    1115         width: 148px;
    1116         height: 148px;
    1117         margin: 0 auto 14px;
     1523        width: 168px;
     1524        height: 168px;
     1525        margin: 16px auto 24px;
    11181526        border-radius: 50%;
    1119         background: conic-gradient(var(--vw-primary) calc(var(--pct) * 1%), #dcdcde 0);
     1527        background: conic-gradient(var(--vw-primary) calc(var(--pct) * 1%), var(--vw-border) 0);
    11201528        display: flex;
    11211529        align-items: center;
    … …  
    11241532
    11251533.velowp-donut__center{
    1126         width: 104px;
    1127         height: 104px;
    1128         border: 1px solid var(--vw-border-soft);
     1534        width: 120px;
     1535        height: 120px;
    11291536        border-radius: 50%;
    11301537        background: var(--vw-surface);
    … …  
    11371544
    11381545.velowp-donut__pct{
    1139         font-size: 24px;
     1546        font-size: 26px;
    11401547        font-weight: 600;
    11411548}
    … …  
    11531560.velowp-bars__row{
    11541561        display: grid;
    1155         grid-template-columns: 78px minmax(0, 1fr) 90px;
     1562        grid-template-columns: 78px minmax(0, 1fr) 64px;
    11561563        gap: 10px;
    11571564        align-items: center;
    … …  
    11931600        display: grid;
    11941601        grid-template-columns: repeat(2, minmax(0, 1fr));
    1195         gap: 10px;
     1602        gap: 16px;
    11961603}
    11971604
    … …  
    12041611}
    12051612
    1206 .velowp-folder-row--active td{ background: #f0f6ff; }
    1207 .velowp-folder-row--stopped td{ background: #fff8e6; }
    1208 .velowp-folder-status{ margin-left: 6px; color: #50575e; font-size: 11px; font-weight: 600; }
     1613.velowp-folder-row--active td{ background: var(--vw-info-bg); }
     1614.velowp-folder-row--stopped td{ background: var(--vw-surface-muted); }
     1615.velowp-folder-status{ margin-left: 6px; color: var(--vw-muted); font-size: 11px; font-weight: 600; }
    12091616.velowp-folder-more-wrap{ margin: 10px 0 0; }
    12101617
    … …  
    12631670}
    12641671
    1265 #velowp-mass.is-running .velowp-mass-mode,
    1266 #velowp-mass.is-running #velowp-mass-folder-field,
    1267 #velowp-mass.is-running #velowp-mass-theme-field{
     1672.velowp-mass.is-running .velowp-mass-mode,
     1673.velowp-mass.is-running .velowp-mass-folder{
    12681674        opacity: .7;
    12691675}
    … …  
    12721678.velowp-table-tight td{
    12731679        vertical-align: middle;
     1680        padding: 12px;
     1681        font-size: 14px;
     1682}
     1683
     1684.wrap.velowp .widefat{
     1685        border-color: var(--vw-border-soft);
     1686        border-radius: var(--vw-radius);
     1687        box-shadow: none;
     1688}
     1689
     1690.wrap.velowp .widefat th{
     1691        font-weight: 600;
     1692}
     1693
     1694.wrap.velowp .widefat td,
     1695.wrap.velowp .widefat th{
     1696        color: var(--vw-text);
     1697        border-bottom: 1px solid var(--vw-border-soft);
     1698}
     1699
     1700.wrap.velowp .widefat tr:last-child td,
     1701.wrap.velowp .widefat tr:last-child th{
     1702        border-bottom: 0;
     1703}
     1704
     1705.velowp-table-scroll{
     1706        max-width: 100%;
     1707        overflow-x: auto;
     1708}
     1709
     1710.velowp-table-scroll:focus-visible{
     1711        outline: 2px solid var(--vw-primary);
     1712        outline-offset: 2px;
     1713}
     1714
     1715.velowp-table-scroll > table{
     1716        min-width: 760px;
     1717}
     1718
     1719.velowp-panel[data-panel="setup"] .widefat{
     1720        margin-top: 16px;
     1721}
     1722
     1723.velowp-panel[data-panel="setup"] .widefat.striped > tbody > :nth-child(odd){
     1724        background: var(--vw-surface);
    12741725}
    12751726
    … …  
    12931744        .velowp-mass-grid,
    12941745        .velowp-mass-layout,
    1295         .velowp-mass-grid--top{
     1746        .velowp-mass-grid--top,
     1747        .velowp-panel[data-panel="setup"] > .velowp-grid{
    12961748                grid-template-columns: 1fr;
     1749        }
     1750        .velowp-home-status-grid,
     1751        .velowp-help-grid,
     1752        .velowp-audit-metrics{
     1753                grid-template-columns: repeat(2, minmax(0, 1fr));
     1754        }
     1755        .velowp-audit-metrics:not(.velowp-audit-metrics--no-generated) > .velowp-audit-metric:nth-child(n+5){
     1756                grid-column: auto;
    12971757        }
    12981758}
    … …  
    13171777        }
    13181778
    1319         .velowp-quick-result-row,
    1320         .velowp-bars__row,
    1321         .velowp-summarybox__grid{
     1779        .velowp-quick-result-row{
    13221780                grid-template-columns: 1fr;
    13231781        }
    13241782
    1325         .wrap.velowp .velowp-btn-wide{
    1326                 width: auto;
     1783        .velowp-card,
     1784        .velowp-quick-card,
     1785        .velowp-mass-card,
     1786        .wrap.velowp .velowp-home-card{
     1787                padding: 16px;
    13271788        }
    1328 }
    1329 .wrap.velowp .velowp-mass-card--summary.velowp-statistics-frozen > :not(h3):not([role="status"]){
    1330         opacity: .6;
    1331 }
    1332 .velowp-home-generation-result[data-velowp-home-frozen="1"] [data-velowp-home-summary-group]{
     1789        .velowp-home-accordion{
     1790                padding: 0;
     1791        }
     1792        .velowp-home-accordion > summary,
     1793        .velowp-home-accordion__body{
     1794                padding: 16px;
     1795        }
     1796        .velowp-home-logo__image--wide{ max-width: 180px; }
     1797        .velowp-chip{ padding: 6px 8px; }
     1798        .velowp-chip__icon{ width: 16px; }
     1799        .velowp-home-status-card__head strong{ flex-basis: 120px; }
     1800}
     1801
     1802@media (max-width: 600px){
     1803        .velowp-home-status-grid,
     1804        .velowp-help-grid,
     1805        .velowp-audit-metrics,
     1806        .velowp-audit-metrics--no-generated{
     1807                grid-template-columns: minmax(0, 1fr);
     1808        }
     1809        .velowp-home-status-card{ min-height: 0; }
     1810        .velowp-self-test-item{ flex-wrap: wrap; }
     1811        .velowp-bars__row{ grid-template-columns: 76px minmax(0, 1fr) 60px; gap: 8px; }
     1812}
     1813.wrap.velowp .velowp-mass-card--summary.velowp-statistics-frozen .velowp-optimization-totals > :not(.description),
     1814.wrap.velowp .velowp-home-generation-result[data-velowp-home-frozen="1"] [data-velowp-home-summary-group]{
     1815        /* Dim saved figures only; the freshness notice and file actions remain clear. */
    13331816        opacity: .6;
    13341817}
    … …  
    13581841        filter: none;
    13591842}
     1843/* Internal navigation preserves the WordPress shell and the current confirmed view. */
     1844.velowp.velowp-navigation-loading { cursor: progress; }
     1845.velowp.velowp-navigation-loading > :not(#velowp-navigation-status) { pointer-events: none; opacity: .65; }
     1846.velowp #velowp-navigation-status { margin: 0 0 16px; }
     1847.velowp #velowp-navigation-status[role="status"] {
     1848        position: fixed;
     1849        right: 20px;
     1850        bottom: 20px;
     1851        z-index: 100000;
     1852        max-width: calc(100vw - 40px);
     1853        box-sizing: border-box;
     1854        margin: 0;
     1855        pointer-events: none;
     1856}
     1857
     1858/* One visual language for Home, WebP, AVIF and Help; no controller-specific skin. */
     1859body.wp-admin:has(.wrap.velowp){
     1860        background: #f3f0eb;
     1861}
     1862.wrap.velowp{
     1863        font-variant-numeric: tabular-nums;
     1864}
     1865.wrap.velowp a{
     1866        color: var(--vw-primary);
     1867}
     1868.wrap.velowp a:hover{
     1869        color: var(--vw-primary-dark);
     1870}
     1871.wrap.velowp .button-primary,
     1872.wrap.velowp .button-primary:hover{
     1873        color: #fff;
     1874}
     1875.wrap.velowp input[type="text"],
     1876.wrap.velowp input[type="url"],
     1877.wrap.velowp input[type="number"],
     1878.wrap.velowp input[type="search"],
     1879.wrap.velowp textarea,
     1880.wrap.velowp select{
     1881        background-color: var(--vw-surface);
     1882        color: var(--vw-text);
     1883}
     1884.wrap.velowp input:focus,
     1885.wrap.velowp select:focus,
     1886.wrap.velowp textarea:focus{
     1887        border-color: var(--vw-primary);
     1888        box-shadow: 0 0 0 1px var(--vw-primary);
     1889}
     1890.wrap.velowp input[readonly]{
     1891        background: var(--vw-surface-muted);
     1892        font-family: Consolas, Monaco, monospace;
     1893}
     1894body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-nav__btn[data-panel="mass"],
     1895body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-nav__btn[data-panel="setup"],
     1896body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-nav__btn[data-panel="mass"],
     1897body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-nav__btn[data-panel="setup"],
     1898.velowp-nav__btn.is-active{
     1899        color: var(--vw-text);
     1900        box-shadow: inset 0 2px 0 var(--vw-accent);
     1901}
     1902.velowp-home-accordion > summary,
     1903.velowp-help-faq summary{
     1904        border-radius: var(--vw-panel-radius);
     1905        background: var(--vw-accent-bg);
     1906}
     1907.velowp-home-accordion[open] > summary,
     1908.velowp-help-faq details[open] summary{
     1909        border-radius: var(--vw-panel-radius) var(--vw-panel-radius) 0 0;
     1910        background: var(--vw-accent-bg-open);
     1911}
     1912.velowp-home-accordion > summary::after,
     1913.velowp-help-faq summary::after{
     1914        color: var(--vw-accent);
     1915}
     1916.velowp-home-accordion > summary:hover,
     1917.velowp-help-faq summary:hover{
     1918        background: #ede4da;
     1919}
     1920.velowp-home-status-card[data-velowp-delivery-card]{
     1921        background: var(--vw-surface-muted);
     1922}
     1923.velowp-home-status-card[data-velowp-delivery-card].is-warning{
     1924        background: var(--vw-warning-bg);
     1925}
     1926.velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head{
     1927        margin-bottom: 0;
     1928}
     1929.velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{
     1930        min-height: 44px;
     1931}
     1932.velowp-home-status-card--strategy[data-velowp-delivery-card] .velowp-home-status-card__head{
     1933        margin-bottom: 10px;
     1934}
     1935.wrap.velowp select.velowp-home-badge-select{
     1936        background-color: var(--vw-surface);
     1937        border-color: var(--vw-control-border);
     1938}
     1939.wrap.velowp select.velowp-home-badge-select.is-success{
     1940        background-color: var(--vw-success-bg);
     1941        border-color: var(--vw-success);
     1942        color: var(--vw-success-text);
     1943}
     1944.wrap.velowp .velowp-savings-value.is-success,
     1945.velowp-quick-result-row--savings .velowp-quick-result-value{
     1946        color: var(--vw-success-text);
     1947        font-weight: 600;
     1948}
     1949.wrap.velowp .velowp-savings-value.is-warning{
     1950        color: var(--vw-warning-text);
     1951}
     1952.velowp-home-generation-result [data-velowp-home-phase]:not([hidden]){
     1953        display: inline-flex;
     1954        align-items: center;
     1955        padding: 3px 8px;
     1956        border-radius: var(--vw-radius);
     1957        background: var(--vw-surface-muted);
     1958        color: var(--vw-muted);
     1959}
     1960.velowp-home-generation-result [data-velowp-home-phase="running"]:not([hidden]),
     1961.velowp-home-generation-result [data-velowp-home-phase="preparing"]:not([hidden]),
     1962.velowp-home-generation-result [data-velowp-home-phase="stopping"]:not([hidden]){
     1963        background: var(--vw-info-bg);
     1964        color: var(--vw-primary-dark);
     1965}
     1966.velowp-home-generation-result [data-velowp-home-phase="completed"]:not([hidden]){
     1967        background: var(--vw-success-bg);
     1968        color: var(--vw-success-text);
     1969}
     1970.velowp-home-generation-result [data-velowp-home-phase="failed"]:not([hidden]){
     1971        background: var(--vw-danger-bg);
     1972        color: var(--vw-danger-text);
     1973}
     1974.velowp-home-generation-result [data-velowp-home-notice],
     1975.velowp-statistics-frozen > [role="status"],
     1976.velowp-self-test-result > p.description:only-child,
     1977.velowp-url-audit-result > p.description:only-child{
     1978        padding: 10px 12px;
     1979        border-radius: var(--vw-radius);
     1980        background: var(--vw-info-bg);
     1981        color: var(--vw-primary-dark);
     1982}
     1983.velowp-self-test-result > p.description:only-child::before,
     1984.velowp-url-audit-result > p.description:only-child::before{
     1985        content: "\f348";
     1986        display: inline-block;
     1987        margin-right: 8px;
     1988        font: normal 16px/1 dashicons;
     1989        vertical-align: -2px;
     1990        color: var(--vw-primary);
     1991}
     1992.velowp-home-status-card[data-velowp-delivery-card].is-warning #velowp-delivery-description,
     1993.velowp-home-status-card[data-velowp-delivery-card].is-warning > p.description{
     1994        color: var(--vw-warning-text);
     1995}
     1996.wrap.velowp .velowp-home-accordion__body > .form-table{
     1997        margin-top: 16px;
     1998        border-spacing: 0;
     1999        border-radius: var(--vw-panel-radius);
     2000        background: var(--vw-surface-muted);
     2001}
     2002.wrap.velowp .velowp-home-accordion__body > .form-table th,
     2003.wrap.velowp .velowp-home-accordion__body > .form-table td{
     2004        padding: 16px;
     2005}
     2006.velowp-url-audit-form + p.description + p.description{
     2007        padding: 10px 12px;
     2008        border-radius: var(--vw-radius);
     2009        background: var(--vw-warning-bg);
     2010        color: var(--vw-warning-text);
     2011}
     2012.wrap.velowp .notice{
     2013        border: 1px solid var(--vw-border-soft);
     2014        border-radius: var(--vw-radius);
     2015        background: var(--vw-surface-muted);
     2016        box-shadow: none;
     2017}
     2018.wrap.velowp .notice-success{
     2019        background: var(--vw-success-bg);
     2020        color: var(--vw-success-text);
     2021}
     2022.wrap.velowp .notice-warning{
     2023        background: var(--vw-warning-bg);
     2024        color: var(--vw-warning-text);
     2025}
     2026.wrap.velowp .notice-error{
     2027        background: var(--vw-danger-bg);
     2028        color: var(--vw-danger-text);
     2029}
     2030.wrap.velowp .notice-info{
     2031        background: var(--vw-info-bg);
     2032        color: var(--vw-primary-dark);
     2033}
     2034.wrap.velowp .widefat{
     2035        background: var(--vw-surface);
     2036}
     2037.wrap.velowp .widefat thead th{
     2038        background: var(--vw-accent-bg);
     2039}
     2040.wrap.velowp .widefat.striped > tbody > :nth-child(odd){
     2041        background: var(--vw-surface-muted);
     2042}
     2043.velowp-folders-table tbody tr:not(.velowp-folder-details):hover td{
     2044        background: var(--vw-accent-bg);
     2045}
     2046.velowp-bars__value,
     2047.velowp-summarybox__v,
     2048.velowp-progressline__pct,
     2049.velowp-home-generation-result strong{
     2050        font-variant-numeric: tabular-nums;
     2051}
     2052@media (max-width: 1100px){
     2053        .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{
     2054                min-height: 0;
     2055        }
     2056}
     2057@media (max-width: 782px){
     2058        .wrap.velowp .velowp-home-accordion__body > .form-table th,
     2059        .wrap.velowp .velowp-home-accordion__body > .form-table td{
     2060                padding: 12px;
     2061        }
     2062        .wrap.velowp .velowp-home-accordion__body > .form-table th{
     2063                padding-bottom: 0;
     2064        }
     2065        .wrap.velowp .velowp-home-accordion__body > .form-table td{
     2066                padding-top: 8px;
     2067        }
     2068}
  • veloimg/trunk/assets/js/admin-avif.js

    r3728637 r3739629  
    11(function () {
    22        'use strict';
     3        var nativeSetTimeout = window.setTimeout || globalThis.setTimeout;
     4        var nativeClearTimeout = window.clearTimeout || globalThis.clearTimeout;
     5        function mount(pageScope) {
     6        var setTimeout = pageScope ? pageScope.setTimeout : nativeSetTimeout;
     7        var clearTimeout = pageScope ? pageScope.clearTimeout : nativeClearTimeout;
     8        function listen(target, name, fn, options) {
     9                if (pageScope) pageScope.listen(target, name, fn, options);
     10                else target.addEventListener(name, fn, options);
     11        }
     12        if (pageScope) pageScope.onDispose(function () {
     13                qsa('.velowp-popover').forEach(function (node) { node.remove(); });
     14        });
    315
    416        var running = false;
    … …  
    4658        function clearReconciliation() {
    4759                if (reconciliationTimer) {
    48                         window.clearTimeout(reconciliationTimer);
     60                        clearTimeout(reconciliationTimer);
    4961                        reconciliationTimer = null;
    5062                }
    … …  
    5567        function clearRuntimeClock() {
    5668                if (runtimeClockTimer) {
    57                         window.clearTimeout(runtimeClockTimer);
     69                        clearTimeout(runtimeClockTimer);
    5870                        runtimeClockTimer = null;
    5971                }
    … …  
    6981                        return;
    7082                }
    71                 runtimeClockTimer = window.setTimeout(function () {
     83                runtimeClockTimer = setTimeout(function () {
    7284                        runtimeClockTimer = null;
    7385                        if (!running || String((currentState || {}).current_rel || '') === '') {
    … …  
    88100
    89101        function statisticsFrozen() {
    90                 return !!running || !!stopRequested || !!currentState.active;
     102                return !!(pageScope && !pageScope.active) || !!running || !!stopRequested || !!currentState.active;
    91103        }
    92104
    … …  
    106118                        clearFoldersOverviewRefresh();
    107119                        if (folderDetailsTimer) {
    108                                 window.clearTimeout(folderDetailsTimer);
     120                                clearTimeout(folderDetailsTimer);
    109121                                folderDetailsTimer = null;
    110122                        }
    … …  
    151163                var row = document.querySelector('#velowp-avif-folders-overview tr[data-folder="' + openFolder.folder + '"]');
    152164                if (!row || !row.classList.contains('velowp-folder-row--active')) { return; }
    153                 folderDetailsTimer = window.setTimeout(function () {
     165                folderDetailsTimer = setTimeout(function () {
    154166                        folderDetailsTimer = null;
    155167                        var currentRow = openFolder ? document.querySelector('#velowp-avif-folders-overview tr[data-folder="' + openFolder.folder + '"]') : null;
    … …  
    187199        function clearFoldersOverviewRefresh() {
    188200                if (foldersOverviewTimer) {
    189                         window.clearTimeout(foldersOverviewTimer);
     201                        clearTimeout(foldersOverviewTimer);
    190202                        foldersOverviewTimer = null;
    191203                }
    … …  
    202214                delayMs = parseInt(delayMs || 0, 10);
    203215                delayMs = Math.max(5000, delayMs || foldersOverviewRefreshMs);
    204                 foldersOverviewTimer = window.setTimeout(function () {
     216                foldersOverviewTimer = setTimeout(function () {
    205217                        foldersOverviewTimer = null;
    206218                        if (!folderStatsRuntime.shouldRequestOverview(typeof document !== 'undefined' && document.hidden)) {
    … …  
    333345
    334346        function refreshNonce(done) {
     347                if (pageScope && !pageScope.active) return;
    335348                if (typeof done === 'function') {
    336349                        nonceWaiters.push(done);
    … …  
    378391                        flushNonceWaiters(false, explainAjaxFailure(xhr, 'timeout'));
    379392                };
     393                if (pageScope) pageScope.trackXhr(xhr);
    380394                xhr.send('action=' + encodeURIComponent(settings.nonceAction || 'velowp_avif_mass_nonce'));
    381395        }
    … …  
    395409
    396410        function post(action, data, done, meta) {
     411                if (pageScope && !pageScope.active) return;
    397412                if (avifTestHooks && typeof avifTestHooks.post === 'function') {
    398413                        avifTestHooks.post(action, Object.assign({}, data || {}), done, Object.assign({}, meta || {}));
    … …  
    439454                                nextMeta._retriedNetwork = true;
    440455                                finished = true;
    441                                 window.setTimeout(function () {
     456                                setTimeout(function () {
    442457                                        post(action, data, done, nextMeta);
    443458                                }, 1600);
    … …  
    491506                        failOnce('timeout');
    492507                };
     508                if (pageScope) pageScope.trackXhr(xhr, action);
    493509                xhr.send(encoded.join('&'));
    494510        }
    … …  
    10371053                showMessage((reason ? reason + ' ' : '') + avifText('startWaiting', 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.'), 'info');
    10381054                renderStatisticsFreeze();
    1039                 reconciliationTimer = window.setTimeout(function () {
     1055                reconciliationTimer = setTimeout(function () {
    10401056                        reconciliationTimer = null;
    10411057                        if (token !== runToken || stopRequested || !running) { return; }
    … …  
    10691085        function scheduleReconciliation(token, controlId, delay) {
    10701086                if (reconciliationTimer) {
    1071                         window.clearTimeout(reconciliationTimer);
    1072                 }
    1073                 reconciliationTimer = window.setTimeout(function () {
     1087                        clearTimeout(reconciliationTimer);
     1088                }
     1089                reconciliationTimer = setTimeout(function () {
    10741090                        reconciliationTimer = null;
    10751091                        if (!running || stopRequested || token !== runToken || reconciliationInFlight) {
    … …  
    11551171                        if (data.active) {
    11561172                                var delay = data.note ? 2500 : 250;
    1157                                 window.setTimeout(function () {
     1173                                setTimeout(function () {
    11581174                                        driveRun(token);
    11591175                                }, delay);
    … …  
    11751191                stopRequested = true;
    11761192                clearReconciliation();
    1177                 if (folderDetailsTimer) { window.clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }
     1193                if (folderDetailsTimer) { clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }
    11781194                clearFoldersOverviewRefresh();
    11791195                var token = ++runToken;
    … …  
    13791395                var total = parseInt(summary.total || 0, 10);
    13801396                node.classList.toggle('velowp-hidden', total === 0);
    1381                 node.textContent = total ? avifText('outsideMonthsSummary', 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; AVIF: %3$d; unsuccessful: %4$d; left: %5$d.')
     1397                node.textContent = total ? avifText('outsideMonthsSummary', 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; AVIF: %3$d; unsuccessful: %4$d; left: %5$d.')
    13821398                        .replace('%1$d', total).replace('%2$d', parseInt(summary.processed || 0, 10)).replace('%3$d', parseInt(summary.ready || 0, 10)).replace('%4$d', parseInt(summary.failed || 0, 10)).replace('%5$d', parseInt(summary.remaining || 0, 10)) : '';
    13831399        }
    … …  
    16891705
    16901706        function closeAvifFolderDetails() {
    1691                 if (folderDetailsTimer) { window.clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }
     1707                if (folderDetailsTimer) { clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }
    16921708                var table = byId('velowp-avif-folders-overview');
    16931709                if (!table) {
    … …  
    19962012                }
    19972013                shell.classList.add('velowp-shell--js');
    1998                 function setPanel(panel) {
     2014                var defaultPanel = shell.getAttribute('data-velowp-avif-panel') || 'mass';
     2015                function setPanel(panel, pushHash) {
    19992016                        panel = panel || shell.getAttribute('data-velowp-avif-panel') || 'mass';
    20002017                        shell.setAttribute('data-velowp-avif-panel', panel);
    … …  
    20082025                                el.classList.toggle('is-active', String(el.getAttribute('data-panel')) === panel);
    20092026                        });
     2027                        if (pushHash) window.location.hash = panel;
    20102028                }
    20112029                shell.addEventListener('click', function (event) {
    … …  
    20152033                        }
    20162034                        event.preventDefault();
    2017                         setPanel(String(btn.getAttribute('data-panel') || 'mass'));
    2018                 });
    2019                 setPanel(shell.getAttribute('data-velowp-avif-panel') || 'mass');
     2035                        setPanel(String(btn.getAttribute('data-panel') || 'mass'), true);
     2036                });
     2037                listen(window, 'hashchange', function () {
     2038                        var panel = String(window.location.hash || '').slice(1);
     2039                        if (!panel || panel === 'mass' || panel === 'setup') setPanel(panel || defaultPanel, false);
     2040                });
     2041                var hash = String(window.location.hash || '').slice(1);
     2042                setPanel(hash === 'mass' || hash === 'setup' ? hash : shell.getAttribute('data-velowp-avif-panel') || 'mass', false);
    20202043        }
    20212044
    … …  
    20302053                        });
    20312054                });
    2032                 var select = byId('velowp-avif-candidate-select');
    2033                 var input = byId('velowp-avif-quick-input');
    2034                 var submit = byId('velowp-avif-quick-generate');
    2035                 if (select && input) {
    2036                         select.addEventListener('change', function () {
    2037                                 input.value = select.value || '';
    2038                                 if (submit && submit.getAttribute('data-velowp-avif-ready') === '1') {
    2039                                         submit.disabled = !input.value;
    2040                                 }
    2041                         });
    2042                 }
    20432055        }
    20442056
    … …  
    21172129
    21182130        function init() {
     2131                if (folderStatsRuntime) folderStatsRuntime.initGenerationConcurrency(cfg(), pageScope);
    21192132                var more = byId('velowp-avif-folders-more');
    21202133                setFoldersMoreVisible(false);
    … …  
    21372150                initMass();
    21382151                initPopover();
    2139                 if (!document.documentElement.dataset.velowpAvifFoldersVisibilityBound) {
    2140                         document.documentElement.dataset.velowpAvifFoldersVisibilityBound = '1';
    2141                         document.addEventListener('visibilitychange', function () {
     2152                        listen(document, 'visibilitychange', function () {
    21422153                                if (!document.hidden) { requestFoldersOverview(false); }
    21432154                        });
    2144                 }
    21452155                ensureFoldersOverviewRefresh(foldersOverviewRefreshMs);
    21462156        }
    … …  
    21932203                        activePopover = popover;
    21942204                }
    2195                 document.addEventListener('click', function (event) {
     2205                listen(document, 'click', function (event) {
    21962206                        var button = event.target && event.target.closest ? event.target.closest('.velowp-file-q') : null;
    21972207                        if (button) {
    … …  
    22112221                        closePopover();
    22122222                });
    2213                 document.addEventListener('keydown', function (event) {
     2223                listen(document, 'keydown', function (event) {
    22142224                        if (event.key === 'Escape') {
    22152225                                closePopover();
    22162226                        }
    22172227                });
    2218                 window.addEventListener('scroll', closePopover, true);
    2219                 window.addEventListener('resize', closePopover);
     2228                listen(window, 'scroll', closePopover, true);
     2229                listen(window, 'resize', closePopover);
    22202230        }
    22212231
    … …  
    22362246                        setRuntimeState: function (next) { next = next || {}; running = !!next.running; stopRequested = !!next.stopRequested; runToken = Number(next.runToken || 0); currentSummary = next.currentSummary || null; currentState = next.currentState || {}; allReady = Number(next.allReady || 0); },
    22372247                        getRuntimeState: function () { return { running: running, stopRequested: stopRequested, runToken: runToken, currentSummary: currentSummary, currentState: currentState, allReady: allReady }; },
    2238                         resetRuntimeState: function () { window.clearTimeout(foldersOverviewTimer); window.clearTimeout(folderDetailsTimer); clearReconciliation(); foldersOverviewTimer = null; folderDetailsTimer = null; running = false; stopRequested = false; runToken = 0; lifecycleRequestSequence = 0; lifecycleAppliedRevisions = {}; currentSummary = null; currentState = {}; allReady = 0; avifTestHooks.post = null; }
     2248                        resetRuntimeState: function () { clearTimeout(foldersOverviewTimer); clearTimeout(folderDetailsTimer); clearReconciliation(); foldersOverviewTimer = null; folderDetailsTimer = null; running = false; stopRequested = false; runToken = 0; lifecycleRequestSequence = 0; lifecycleAppliedRevisions = {}; currentSummary = null; currentState = {}; allReady = 0; avifTestHooks.post = null; }
    22392249                };
    22402250                if (window.__VELOIMG_AVIF_SKIP_AUTO_INIT__ === true) { return; }
    22412251        }
    22422252        if (document.readyState === 'loading') {
    2243                 document.addEventListener('DOMContentLoaded', init);
     2253                listen(document, 'DOMContentLoaded', init);
    22442254        } else {
    22452255                init();
    22462256        }
     2257        }
     2258        if (window.VeloimgAdminNavigation) window.VeloimgAdminNavigation.register(['velowp-avif'], mount);
     2259        else mount(null);
    22472260})();
  • veloimg/trunk/assets/js/admin.js

    r3728637 r3739629  
    11(function ($) {
    22        'use strict';
     3        var nativeSetTimeout = window.setTimeout || globalThis.setTimeout;
     4        var nativeClearTimeout = window.clearTimeout || globalThis.clearTimeout;
     5        function mount(pageScope) {
     6        var setTimeout = pageScope ? pageScope.setTimeout : nativeSetTimeout;
     7        var clearTimeout = pageScope ? pageScope.clearTimeout : nativeClearTimeout;
     8        function pageAjax(options) { return pageScope ? pageScope.ajax(options) : $.ajax(options); }
     9        function listen(target, name, fn, options) {
     10                if (pageScope) pageScope.listen(target, name, fn, options);
     11                else target.addEventListener(name, fn, options);
     12        }
     13        if (pageScope) pageScope.onDispose(function () {
     14                $(document).off('.velowpPage'); $(window).off('.velowpPage');
     15                $('.velowp-popover').remove();
     16        });
    317        var folderStatsRuntime = window.VeloimgFolderStatisticsRuntime;
    418
    … …  
    4963                        }
    5064
    51                         window.setTimeout(function () {
     65                        setTimeout(function () {
    5266                                var target = document.getElementById('velowp-diagnostics');
    5367                                if (!target) {
    … …  
    87101                }
    88102
     103                var defaultPanel = String($shell.attr('data-velowp-panel') || 'mass');
    89104                var initial = '';
    90105                if (window.location.hash) {
    … …  
    96111                setPanel(initial, false);
    97112
    98                 $(document).on('click', '.velowp-nav__btn', function (e) {
     113                $(document).on('click.velowpPage', '.velowp-nav__btn', function (e) {
    99114                        e.preventDefault();
    100115                        setPanel($(this).data('panel'), true);
    101116                });
    102117
    103                 $(document).on('click', '[data-velowp-switch]', function (e) {
     118                $(document).on('click.velowpPage', '[data-velowp-switch]', function (e) {
    104119                e.preventDefault();
    105120                setPanel($(this).data('velowp-switch'), true);
    106121        });
    107122
    108                 $(window).on('hashchange', function () {
     123                $(window).on('hashchange.velowpPage', function () {
    109124                        var h = String(window.location.hash || '').replace('#', '');
    110                         if (h) {
    111                                 setPanel(h, false);
    112                         }
     125                        setPanel(h || defaultPanel, false);
    113126                });
    114127        }
    115128
    116129        $(function () {
     130                if (pageScope && !pageScope.active) return;
    117131                initPanels();
     132                if (folderStatsRuntime) folderStatsRuntime.initGenerationConcurrency(window.velowpAdmin || {}, pageScope);
     133
     134                (function initHomeAccordions(){
     135                        if (!$('.velowp-home-accordion').length) return;
     136                        function revealHashTarget(){
     137                                var target = document.getElementById(String(window.location.hash || '').slice(1));
     138                                if (!target) return;
     139                                var $block = $(target).closest('.velowp-home-accordion');
     140                                if (!$block.length) return;
     141                                $block.prop('open', true);
     142                                target.scrollIntoView({ block: 'start' });
     143                        }
     144                        $(window).on('hashchange.velowpHomeAccordion.velowpPage', revealHashTarget);
     145                        revealHashTarget();
     146                })();
    118147
    119148                (function initHomeGenerationStatus(){
    … …  
    125154                        var stopped = false;
    126155                        var visibilityEpoch = 0;
     156                        var acknowledged = {};
    127157
    128158                        function schedule(delay){
    … …  
    130160                                timer = null;
    131161                                if (!stopped && !document.hidden) timer = setTimeout(refresh, delay);
     162                        }
     163                        function refreshSummaries(formats, epoch, refreshed){
     164                                if (stopped || document.hidden || epoch !== visibilityEpoch) { schedule(6000); return; }
     165                                var format = formats.shift();
     166                                if (!format) { schedule(refreshed ? 0 : 6000); return; }
     167                                pending = true;
     168                                var accepted = false;
     169                                pageAjax({
     170                                        url: config.ajaxUrl, type: 'POST', dataType: 'json', timeout: 45000,
     171                                        data: { action: config.optimizationSummaryAction, format: format.name, nonce: config.homeSummaryNonces[format.name] }
     172                                }).done(function(response){
     173                                        if (stopped || document.hidden || epoch !== visibilityEpoch || !response || !response.success || !response.data) return;
     174                                        var summary = response.data.global_summary;
     175                                        if (response.data.ok && summary && summary.home_run_key === format.key) {
     176                                                acknowledged[format.name] = format.key;
     177                                                accepted = true;
     178                                        }
     179                                }).fail(function(xhr){
     180                                        if (xhr && (xhr.status === 400 || xhr.status === 403)) stopped = true;
     181                                }).always(function(){
     182                                        pending = false;
     183                                        // Reconcile a raced/failed refresh through status before trying again.
     184                                        if (accepted) refreshSummaries(formats, epoch, true);
     185                                        else schedule(6000);
     186                                });
    132187                        }
    133188                        function refresh(){
    … …  
    136191                                pending = true;
    137192                                var epoch = visibilityEpoch;
    138                                 $.ajax({
     193                                var formats = [];
     194                                pageAjax({
    139195                                        url: config.ajaxUrl, type: 'POST', dataType: 'json', timeout: 15000,
    140196                                        data: { action: 'velowp_home_status', nonce: config.homeStatusNonce }
    … …  
    160216                                                }
    161217                                        });
     218                                        var phases = response.data.generation_phases || {};
     219                                        var keys = response.data.pending_refresh || {};
     220                                        if (config.optimizationSummaryAction && config.homeSummaryNonces) {
     221                                                ['webp', 'avif'].forEach(function(format){
     222                                                        var key = keys[format];
     223                                                        if ((phases[format] === 'paused' || phases[format] === 'completed') && typeof key === 'string' && key && acknowledged[format] !== key && config.homeSummaryNonces[format]) {
     224                                                                formats.push({ name: format, key: key });
     225                                                        }
     226                                                });
     227                                        }
    162228                                }).fail(function(xhr){
    163229                                        if (xhr && (xhr.status === 400 || xhr.status === 403)) stopped = true;
    164230                                }).always(function(){
    165231                                        pending = false;
    166                                         schedule(6000);
     232                                        refreshSummaries(formats, epoch, false);
    167233                                });
    168234                        }
    169                         $(document).on('visibilitychange', function(){
     235                        $(document).on('visibilitychange.velowpPage', function(){
    170236                                visibilityEpoch++;
    171237                                schedule(0);
    172238                        });
    173                         $(window).on('pagehide', function(){ stopped = true; visibilityEpoch++; schedule(0); });
     239                        $(window).on('pagehide.velowpPage', function(){ stopped = true; visibilityEpoch++; schedule(0); });
    174240                        schedule(6000);
    175241                })();
    … …  
    183249                }, 5000);
    184250
    185                 $(document).on('input change', 'input[type="range"][data-velowp-range-output]', function () {
     251                $(document).on('input.velowpPage change.velowpPage', 'input[type="range"][data-velowp-range-output]', function () {
    186252                        var outputId = String($(this).data('velowp-range-output') || '');
    187253                        var val = String($(this).val() || '');
    … …  
    191257                });
    192258
    193                 $(document).on('input change', 'input[type="range"][data-velowp-range]', function () {
     259                $(document).on('input.velowpPage change.velowpPage', 'input[type="range"][data-velowp-range]', function () {
    194260                        var key = String($(this).data('velowp-range') || '');
    195261                        if (!key) return;
    … …  
    211277                                        $note.prop('hidden', String($note.data('velowp-delivery-note') || '') !== method);
    212278                                });
     279                                var status = method === 'off' ? 'is-warning' : 'is-success';
     280                                $('[data-velowp-delivery-card], #velowp-delivery-method, #velowp-image-strategy')
     281                                        .removeClass('is-success is-warning is-error').addClass(status);
    213282                        }
    214283
    … …  
    217286                })();
    218287
    219                 $(document).on('click', '.velowp-details > summary', function (e) {
     288                (function initHomeStrategyNote(){
     289                        var $select = $('#velowp-image-strategy');
     290                        var $note = $('#velowp-strategy-description');
     291                        if (!$select.length || !$note.length) return;
     292                        function syncStrategyNote(){
     293                                $note.text(String($select.find('option:selected').attr('data-description') || ''));
     294                        }
     295                        $select.on('change', syncStrategyNote);
     296                        syncStrategyNote();
     297                })();
     298
     299                $(document).on('click.velowpPage', '.velowp-details > summary', function (e) {
    220300                        e.preventDefault();
    221301                        var $d = $(this).closest('details');
    … …  
    223303                });
    224304
    225                 var $input = $('#velowp-quick-input');
     305                ['', 'avif-'].forEach(function (prefix) {
     306                var $input = $('#velowp-' + prefix + 'quick-input');
    226307                if ($input.length) {
    227                         var $quickSubmit = $('#velowp-quick-generate');
    228                         var $fallbackInput = $('#velowp-quick-input-fallback');
     308                        var $quickSubmit = $('#velowp-' + prefix + 'quick-generate');
     309                        var $fallbackInput = $('#velowp-' + prefix + 'quick-input-fallback');
    229310
    230311                        function syncQuickSubmitState() {
    … …  
    233314                                var ready = hiddenVal.length > 0 || fallbackVal.length > 0;
    234315                                if ($quickSubmit.length) {
    235                                         $quickSubmit.prop('disabled', !ready);
     316                                        $quickSubmit.prop('disabled', !ready || $quickSubmit.attr('data-velowp-avif-ready') === '0');
    236317                                }
    237318                        }
    238319
    239320                        syncQuickSubmitState();
    240                         if ($('#velowp-candidate-select').length) {
    241                                 $('#velowp-candidate-select').find('option').first().text(velowpText('selectImage', 'Select an image'));
    242                         }
    243 
    244                         $('#velowp-candidate-select').on('change', function () {
     321                        if ($('#velowp-' + prefix + 'candidate-select').length) {
     322                                $('#velowp-' + prefix + 'candidate-select').find('option').first().text(velowpText('selectImage', 'Select an image'));
     323                        }
     324
     325                        $('#velowp-' + prefix + 'candidate-select').on('change', function () {
    245326                                var v = String($(this).val() || '');
    246327                                $input.val(v);
    … …  
    248329                        });
    249330
    250                         $('#velowp-pick-media').on('click', function (e) {
     331                        $('#velowp-' + prefix + 'pick-media').on('click', function (e) {
    251332                                e.preventDefault();
    252333                                if (typeof wp === 'undefined' || !wp.media) {
    … …  
    286367                        }
    287368                }
     369                });
     370
     371                (function initBypassCopy(){
     372                        var $field = $('#velowp-bypass-query');
     373                        var $status = $('#velowp-bypass-copy-status');
     374                        if (!$field.length) return;
     375                        function report(copied){
     376                                $status.text(String($status.attr(copied ? 'data-success' : 'data-fallback') || ''));
     377                        }
     378                        function fallback(){
     379                                $field[0].focus();
     380                                $field[0].select();
     381                                var copied = false;
     382                                try { copied = !!document.execCommand && document.execCommand('copy'); } catch (e) {}
     383                                report(copied);
     384                        }
     385                        function copy(){
     386                                $field[0].select();
     387                                if (window.navigator.clipboard && typeof window.navigator.clipboard.writeText === 'function') {
     388                                        window.navigator.clipboard.writeText(String($field.val() || '')).then(function(){ report(true); }, fallback);
     389                                } else {
     390                                        fallback();
     391                                }
     392                        }
     393                        $field.on('click', copy);
     394                        $('#velowp-bypass-copy').on('click', copy);
     395                })();
    288396
    289397                (function initSelfTest(){
    … …  
    356464                                $result.html('<p class="description velowp-muted">' + velowpEscapeHtml(velowpText('selfTestLoading', 'Running self-test...')) + '</p>');
    357465
    358                                 $.ajax({
     466                                pageAjax({
    359467                                        method: 'POST',
    360468                                        url: ajaxUrl,
    … …  
    522630                                $result.html('<p class="description velowp-muted">' + velowpEscapeHtml(velowpText('frontendAuditLoading', 'Checking the page...')) + '</p>');
    523631
    524                                 $.ajax({
     632                                pageAjax({
    525633                                        method: 'POST',
    526634                                        url: ajaxUrl,
    … …  
    550658                })();
    551659
    552                 $(document).on('change', '.velowp-scan-candidate', function () {
     660                $(document).on('change.velowpPage', '.velowp-scan-candidate', function () {
    553661                        var v = String($(this).val() || '');
    554662                        if (v) {
    … …  
    608716                        var $badges = $('#velowp-mass-badges');
    609717                        var $help = $('#velowp-mass-help');
     718                        var $retryCard = $('#velowp-webp-retry');
     719                        var $retryStart = $('#velowp-webp-retry-start');
     720                        var $retryQuality = $('#velowp-webp-retry-quality');
     721                        var $retryEngine = $('#velowp-webp-retry-engine');
     722                        var webpRetrySnapshot = { scope: '', count: 0 };
    610723                        var $outsideMonthsSummary = $('#velowp-outside-months-summary');
    611724
    … …  
    723836
    724837                        function statisticsFrozen() {
    725                                 return !webpControlState || !!webpCommandPending || webpControlUncertain || webpStopUnconfirmed
     838                                return !!(pageScope && !pageScope.active) || !webpControlState || !!webpCommandPending || webpControlUncertain || webpStopUnconfirmed
    726839                                        || ['preparing', 'running', 'stopping'].indexOf(webpPhase(webpControlState)) !== -1;
    727840                        }
    … …  
    818931                                renderStatisticsFreeze();
    819932                                applyModeUI();
     933                                renderWebpRetry();
    820934                                $stop.prop('disabled', !!webpCommandPending || webpControlUncertain || !state.control_id || (phase !== 'running' && phase !== 'preparing'));
    821935                                var label = '', buttonLabel = '';
    … …  
    857971                                        if (statusText.indexOf(webpControlError + ' ') !== 0) $status.text(webpControlError + ' ' + statusText);
    858972                                }
     973                        }
     974
     975                        function renderWebpRetry() {
     976                                var state = webpControlState || {};
     977                                var ownRetry = !!state.retry && !!state.retry.quality && isSameRunScope(state, sourceValue(), folderValue());
     978                                var activeRetry = ownRetry && ['preparing', 'running', 'stopping'].indexOf(webpPhase(state)) !== -1;
     979                                var count = webpRetrySnapshot.scope === currentWebpScopeKey() ? webpRetrySnapshot.count : 0;
     980                                if (activeRetry) count = progressInt(state.manifest_count || state.display_total || state.total);
     981                                $retryCard.toggleClass('velowp-hidden', count <= 0);
     982                                $('#velowp-webp-retry-count').text(massText('retryCount', 'Files eligible for retry in this selection: %d').replace('%d', count));
     983                                var disabled = statisticsFrozen() || running || !massStateReady || webpStatsFlight === webpUiEpoch;
     984                                $retryCard.find('input, select').prop('disabled', disabled);
     985                                $retryStart.prop('disabled', disabled || count <= 0 || $retryCard.attr('data-encoder-ready') !== '1')
     986                                        .text(massText('retryStart', 'Retry processing %d files').replace('%d', count));
     987                                if (activeRetry) {
     988                                        $retryQuality.val(state.retry.quality);
     989                                        $retryEngine.val(state.retry.engine);
     990                                }
     991                                $('#velowp-webp-retry-quality-value').text(String($retryQuality.val() || 75) + '%');
     992                                var hint = disabled ? massText(activeRetry ? 'retryRunning' : 'retryUnavailable', activeRetry
     993                                        ? 'Retry processing is running. Progress and Stop are in the generation block above.'
     994                                        : 'Retry processing will be available after statistics are updated and generation is stopped or completed.') : '';
     995                                $('#velowp-webp-retry-status').toggleClass('velowp-hidden', !hint).text(hint);
    859996                        }
    860997
    … …  
    11811318                                        return;
    11821319                                }
    1183                                 var text = massText('outsideMonthsSummary', 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.')
     1320                                var text = massText('outsideMonthsSummary', 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.')
    11841321                                        .replace('%1$d', total).replace('%2$d', parseInt(outside.processed || 0, 10))
    11851322                                        .replace('%3$d', parseInt(outside.ready || 0, 10)).replace('%4$d', parseInt(outside.failed || 0, 10)).replace('%5$d', parseInt(outside.remaining || 0, 10));
    … …  
    13771514                                        return;
    13781515                                }
    1379                                 nonceRefreshXhr = $.ajax({
     1516                                nonceRefreshXhr = pageAjax({
    13801517                                        url: ajaxUrl,
    13811518                                        type: 'POST',
    … …  
    14101547                                data.action = action;
    14111548                                data.nonce = nonce;
    1412                                 $.ajax({
     1549                                pageAjax({
    14131550                                        url: ajaxUrl,
    14141551                                        type: 'POST',
    … …  
    18241961
    18251962                        setFoldersMoreVisible(false);
    1826                         $(document).off('click.velowpFoldersMore').on('click.velowpFoldersMore', '#velowp-folders-more', function () {
     1963                        $(document).off('click.velowpFoldersMore.velowpPage').on('click.velowpFoldersMore.velowpPage', '#velowp-folders-more', function () {
    18271964                                if (statisticsFrozen() || folderMorePending) { return; }
    18281965                                folderHistoryAutoContinued = false;
    … …  
    21702307}
    21712308
    2172 $(document).off('click.velowpSort').on('click.velowpSort', '#velowp-folders-overview .velowp-folder-details th.velowp-sort', function(e){
     2309$(document).off('click.velowpSort.velowpPage').on('click.velowpSort.velowpPage', '#velowp-folders-overview .velowp-folder-details th.velowp-sort', function(e){
    21732310        e.preventDefault();
    21742311        if (!openFolder) return;
    … …  
    23612498                                return isThemeMode()
    23622499                                        ? massText('emptyThemeFolder', 'No JPG/PNG images were found in the selected theme folder.')
    2363                                         : massText('emptyFolder', 'No images were found in the Media Library for this folder.');
     2500                                        : massText('emptyFolder', 'No images were found in uploads for this folder.');
    23642501                        }
    23652502
    … …  
    23692506                                }
    23702507                                return isAllMode()
    2371                                         ? massText('startAll', 'Start for the entire Media Library')
     2508                                        ? massText('startAll', 'Start for all uploads')
    23722509                                        : massText('startFolder', 'Start generation');
    23732510                        }
    … …  
    24952632                                } else {
    24962633
    2497                                 $start.text(all ? massText('startAll', 'Start for the entire Media Library') : massText('startFolder', 'Start generation'));
     2634                                $start.text(all ? massText('startAll', 'Start for all uploads') : massText('startFolder', 'Start generation'));
    24982635                                }
    24992636                                if (theme && (controlsLocked || massStateReady)) {
    … …  
    26692806
    26702807                        function requestStats() {
     2808                                renderWebpRetry();
    26712809                                if (webpCommandPending) return;
    26722810                                if (webpStatsFlight === webpUiEpoch) return;
    … …  
    27072845                                updateSecondaryActions(folderVal, {});
    27082846                                webpStatsFlight = requestedEpoch;
     2847                                renderWebpRetry();
    27092848                                if (!statisticsFrozen()) $('#velowp-statistics-status').removeClass('velowp-hidden').text(massText('statsUpdating', 'Updating statistics...'));
    27102849                                ajax('velowp_mass_stats', { folder: folderVal, source: sourceValue(), include_overview: '1', history_limit: folderHistoryLimit }, function (d) {
    … …  
    28643003                                        renderOutsideMonthsSummary(summary);
    28653004                                        renderHelpBlock(summary);
     3005                                        webpRetrySnapshot = { scope: currentWebpScopeKey(), count: progressInt(summary.retryable) };
     3006                                        renderWebpRetry();
    28663007
    28673008                                       
    … …  
    29183059                                                        $status.text(massText('emptyThemeFolder', 'No JPG/PNG images were found in the selected theme folder.'));
    29193060                                                } else {
    2920                                                         $status.text(massText('emptyFolder', 'No images were found in the Media Library for this folder.'));
     3061                                                        $status.text(massText('emptyFolder', 'No images were found in uploads for this folder.'));
    29213062                                                }
    29223063                                        } else {
    … …  
    30383179                                        payload.source = sourceValue();
    30393180                                        payload.mode = mode === 'repair' ? 'repair' : 'generate';
     3181                                        if (action === 'start' && mode === 'retry') {
     3182                                                payload.retry_quality = String($retryQuality.val() || 75);
     3183                                                payload.retry_engine = String($retryEngine.val() || 'auto');
     3184                                                payload.display_total = webpRetrySnapshot.count;
     3185                                                payload.display_ready = payload.display_failed = payload.display_processed = 0;
     3186                                        }
    30403187                                }
    30413188                                var epoch = bumpWebpUiEpoch();
    … …  
    31273274                                }
    31283275                                startRun('generate');
     3276                        });
     3277                        $retryQuality.on('input change', function () { $('#velowp-webp-retry-quality-value').text(String(this.value) + '%'); });
     3278                        $retryStart.on('click', function (e) {
     3279                                e.preventDefault();
     3280                                if ($retryStart.prop('disabled') || webpRetrySnapshot.scope !== currentWebpScopeKey() || webpRetrySnapshot.count <= 0) return;
     3281                                runWebpCommand('start', 'retry');
    31293282                        });
    31303283                        if ($repair.length) {
    … …  
    32003353                        }
    32013354
    3202                         $(window).on('focus', function () {
     3355                        $(window).on('focus.velowpPage', function () {
    32033356                                refreshMassStateFromServer();
    32043357                        });
    32053358                        if (document && document.addEventListener) {
    3206                                         document.addEventListener('visibilitychange', function () {
     3359                                        listen(document, 'visibilitychange', function () {
    32073360                                        if (!document.hidden) {
    32083361                                                refreshMassStateFromServer();
    … …  
    32213374                        renderGlobalSummary();
    32223375                        applyModeUI();
    3223                         setTimeout(requestStats, 80);
     3376                        setTimeout(requestStats, 0);
    32243377                        // The first status response establishes whether reads are safe and carries the overview.
    32253378                })();   });
    … …  
    32563409                                activePopover = $p;
    32573410                        }
    3258                         $(document).on('click', '.velowp-file-q, .velowp-help-q', function(e){
     3411                        $(document).on('click.velowpPage', '.velowp-file-q, .velowp-help-q', function(e){
    32593412                                e.preventDefault();
    32603413                                e.stopPropagation();
    … …  
    32673420                                openPopover($btn, text);
    32683421                        });
    3269                         $(document).on('click', function(e){
     3422                        $(document).on('click.velowpPage', function(e){
    32703423                                if (e && $(e.target).closest('.velowp-popover, .velowp-file-q, .velowp-help-q').length) return;
    32713424                                closePopover();
    32723425                        });
    3273                         $(document).on('keydown', function(e){ if (e && e.key === 'Escape') closePopover(); });
    3274                         $(window).on('scroll resize', function(){ if (activePopover) closePopover(); });
    3275 
     3426                        $(document).on('keydown.velowpPage', function(e){ if (e && e.key === 'Escape') closePopover(); });
     3427                        $(window).on('scroll.velowpPage resize.velowpPage', function(){ if (activePopover) closePopover(); });
     3428
     3429        }
     3430        if (window.VeloimgAdminNavigation) window.VeloimgAdminNavigation.register(['velowp', 'velowp-webp', 'velowp-help'], mount);
     3431        else mount(null);
    32763432})(jQuery);
  • veloimg/trunk/assets/js/folder-statistics-runtime.js

    r3728637 r3739629  
    7474        function overviewVisible(hasRows, renderedSource, renderedFolder, source, folder) {
    7575                return !!hasRows && (renderedSource || 'uploads') === source && (source !== 'theme' || renderedFolder === folder);
     76        }
     77
     78        var dismissedGenerations = {};
     79        /** One display-only warning for both generation pages; never changes controls. */
     80        function initGenerationConcurrency(config, pageScope) {
     81                if (typeof document === 'undefined') return;
     82                var node = document.querySelector('[data-velowp-concurrency-warning]');
     83                if (!node || !config.ajaxUrl || !config.homeStatusNonce || node.velowpConcurrencyInitialized) return;
     84                var other = node.getAttribute('data-velowp-other-format');
     85                if (other !== 'webp' && other !== 'avif') return;
     86                node.velowpConcurrencyInitialized = true;
     87                var generationId = node.getAttribute('data-velowp-generation-id') || '';
     88                var storageKey = 'veloimg:concurrency-dismissed:' + config.ajaxUrl + ':' + other;
     89                var dismissed = dismissedGenerations[storageKey] || '';
     90                try { dismissed = window.sessionStorage.getItem(storageKey) || dismissed; } catch (error) { /* Storage may be disabled; AJAX navigation still keeps the dismissal. */ }
     91                var unknownDismissed = false;
     92                if (generationId && dismissed === generationId) node.hidden = true;
     93                function dismiss() {
     94                        node.hidden = true;
     95                        if (!generationId) { unknownDismissed = true; return; }
     96                        dismissed = generationId;
     97                        dismissedGenerations[storageKey] = generationId;
     98                        try { window.sessionStorage.setItem(storageKey, generationId); } catch (error) { /* Keep the in-memory dismissal. */ }
     99                }
     100                var close = node.querySelector('[data-velowp-concurrency-dismiss]');
     101                if (close) {
     102                        if (pageScope) pageScope.listen(close, 'click', dismiss);
     103                        else close.addEventListener('click', dismiss);
     104                }
     105                var timer = null, pending = false, stopped = false, epoch = 0;
     106                if (pageScope) pageScope.onDispose(function () { stopped = true; epoch++; if (timer !== null) clearTimeout(timer); });
     107                function schedule(delay) {
     108                        if (timer !== null) clearTimeout(timer);
     109                        timer = null;
     110                        if (!stopped && !document.hidden) timer = setTimeout(refresh, delay);
     111                }
     112                function refresh() {
     113                        timer = null;
     114                        if (stopped || document.hidden || pending) return;
     115                        pending = true;
     116                        var requestEpoch = epoch, xhr = new XMLHttpRequest();
     117                        xhr.open('POST', config.ajaxUrl, true);
     118                        xhr.timeout = 15000;
     119                        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
     120                        function finish() {
     121                                pending = false;
     122                                if (xhr.status === 400 || xhr.status === 403) stopped = true;
     123                                if (!stopped && !document.hidden && requestEpoch === epoch && xhr.status >= 200 && xhr.status < 300) {
     124                                        try {
     125                                                var response = JSON.parse(xhr.responseText || '{}');
     126                                                var phase = response.success && response.data && response.data.generation_phases && response.data.generation_phases[other];
     127                                                // Missing/failed reads retain the last warning instead of claiming idle.
     128                                                if (['idle', 'preparing', 'running', 'stopping', 'paused', 'failed', 'completed'].indexOf(phase) !== -1) {
     129                                                        var ids = response.data.generation_ids;
     130                                                        if (ids && typeof ids[other] === 'string') {
     131                                                                if (ids[other] !== generationId) unknownDismissed = false;
     132                                                                generationId = ids[other];
     133                                                        }
     134                                                        if (phase === 'idle' || phase === 'completed' || phase === 'failed') unknownDismissed = false;
     135                                                        var active = ['preparing', 'running', 'stopping'].indexOf(phase) !== -1;
     136                                                        node.hidden = !active || unknownDismissed || !!(generationId && dismissed === generationId);
     137                                                }
     138                                        } catch (error) { /* Keep the last confirmed display state. */ }
     139                                }
     140                                schedule(6000);
     141                        }
     142                        xhr.onload = xhr.onerror = xhr.ontimeout = finish;
     143                        xhr.send('action=velowp_home_status&nonce=' + encodeURIComponent(config.homeStatusNonce));
     144                }
     145                var visible = function () { epoch++; schedule(0); };
     146                var leave = function () { stopped = true; epoch++; schedule(0); };
     147                if (pageScope) {
     148                        pageScope.listen(document, 'visibilitychange', visible);
     149                        pageScope.listen(window, 'pagehide', leave);
     150                } else {
     151                        document.addEventListener('visibilitychange', visible);
     152                        window.addEventListener('pagehide', leave);
     153                }
     154                schedule(0);
    76155        }
    77156
    … …  
    119198                overviewRefreshDelay: 6000,
    120199                overviewVisible: overviewVisible,
     200                initGenerationConcurrency: initGenerationConcurrency,
    121201                renderOptimizationSummary: renderOptimizationSummary,
    122202                refreshOptimizationSummary: refreshOptimizationSummary,
  • veloimg/trunk/languages/veloimg-ru_RU.po

    r3728637 r3739629  
    22msgid ""
    33msgstr ""
    4 "Project-Id-Version: VeloIMG – WebP & AVIF Image Optimizer 0.9.6\n"
     4"Project-Id-Version: VeloIMG – WebP & AVIF Image Optimizer 0.9.7\n"
    55"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/veloimg\n"
    66"POT-Creation-Date: 2026-09-27T08:44:59+00:00\n"
    … …  
    33283328msgid "%1$d processed · %2$d optimized · %3$d unsuccessful"
    33293329msgstr "%1$d обработано · %2$d оптимизировано · %3$d неудачно"
     3330
     3331#: templates/admin-home.php
     3332msgid "Diagnostics"
     3333msgstr "Диагностика"
     3334
     3335#: templates/admin-diagnostics.php
     3336msgid "Trial WebP"
     3337msgstr "Пробный WebP"
     3338
     3339#: templates/admin-diagnostics.php
     3340msgid "Trial AVIF"
     3341msgstr "Пробный AVIF"
     3342
     3343#: templates/admin-home.php
     3344msgid "View original images"
     3345msgstr "Просмотр оригиналов"
     3346
     3347#: templates/admin-home.php
     3348msgid "Copy parameter"
     3349msgstr "Скопировать параметр"
     3350
     3351#: templates/admin-home.php
     3352msgid "Copied."
     3353msgstr "Скопировано."
     3354
     3355#: templates/admin-home.php
     3356msgid "Parameter selected. Copy it with Ctrl+C or Command+C."
     3357msgstr "Параметр выделен. Скопируйте его сочетанием Ctrl+C или Command+C."
     3358
     3359#: templates/admin-home.php
     3360msgid "Copy the parameter and append it to the page address to view the original images. Saved settings are not changed."
     3361msgstr "Скопируйте параметр и добавьте его в конец адреса страницы, чтобы увидеть оригинальные изображения. Сохранённые настройки не меняются."
     3362
     3363#: templates/admin-home.php
     3364msgid "Copy the parameter and append it to the JPG/PNG image address to view the original. Saved settings are not changed."
     3365msgstr "Скопируйте параметр и добавьте его в конец адреса JPG/PNG-изображения, чтобы увидеть оригинал. Сохранённые настройки не меняются."
     3366
     3367#: templates/admin-home.php
     3368msgid "Controls how much diagnostic detail is written to the server error log. Leave Error for normal use; Warning and Info help investigate problems, for example at support request. If WordPress debug logging is enabled, messages appear in wp-content/debug.log."
     3369msgstr "Определяет подробность записей в журнале ошибок сервера. Для обычной работы оставьте «Ошибки». «Предупреждения» и «Информация» помогают разобраться в проблемах, например по просьбе поддержки. Если в WordPress включён журнал отладки, записи доступны в wp-content/debug.log."
     3370
     3371#: templates/admin-dashboard.php
     3372msgid "Configure generation here. Single-file tests are in Diagnostics on the VeloIMG home page."
     3373msgstr "Здесь настраивается генерация. Пробная обработка одного файла доступна в блоке «Диагностика» на главной странице VeloIMG."
     3374
     3375#: templates/webp-retry.php
     3376msgid "Retry unsuccessful WebP files"
     3377msgstr "Повторная обработка WebP"
     3378
     3379#: templates/webp-retry.php
     3380msgid "Retry quality"
     3381msgstr "Качество повторного сжатия"
     3382
     3383#: templates/webp-retry.php
     3384msgid "Encoder"
     3385msgstr "Движок"
     3386
     3387#: templates/webp-retry.php
     3388msgid "Automatic"
     3389msgstr "Авто"
     3390
     3391#: templates/webp-retry.php
     3392msgid "Lossy compression is used for this retry, including PNG. Original files, successful WebP files and your main settings are not changed."
     3393msgstr "Повторное сжатие с потерями, в том числе для PNG. Оригиналы, успешно созданные WebP и основные настройки не изменяются."
     3394
     3395#: templates/webp-retry.php
     3396msgid "Retry processing"
     3397msgstr "Повторить обработку"
     3398
     3399#: src/Admin/SettingsPage.php
     3400#, php-format
     3401msgid "Files eligible for retry in this selection: %d"
     3402msgstr "Можно обработать повторно в выбранной области: %d файлов"
     3403
     3404#: src/Admin/SettingsPage.php
     3405#, php-format
     3406msgid "Retry processing %d files"
     3407msgstr "Повторить обработку %d файлов"
     3408
     3409#: src/Admin/SettingsPage.php
     3410msgid "Retry processing is running. Progress and Stop are in the generation block above."
     3411msgstr "Идёт повторная обработка. Прогресс и кнопка остановки — в блоке генерации выше."
     3412
     3413#: src/Admin/SettingsPage.php
     3414msgid "Retry processing will be available after statistics are updated and generation is stopped or completed."
     3415msgstr "Повторная обработка станет доступна после обновления статистики и остановки или завершения генерации."
     3416
     3417#: src/Admin/SettingsPage.php
     3418msgid "No unsuccessful WebP files eligible for retry remain in this selection. Refresh the statistics."
     3419msgstr "В выбранной области больше нет неудачных WebP, подходящих для повторной обработки. Обновите статистику."
     3420
     3421#: src/Admin/SettingsPage.php
     3422msgid "Choose a retry quality between 50 and 100."
     3423msgstr "Выберите качество повторного сжатия от 50 до 100."
     3424
     3425#: src/Admin/SettingsPage.php
     3426msgid "The selected WebP encoder is unavailable. Refresh the page and select an available encoder."
     3427msgstr "Выбранный движок WebP недоступен. Обновите страницу и выберите доступный движок."
     3428
     3429#: templates/admin-dashboard.php templates/admin-avif.php src/Admin/SettingsPage.php
     3430msgid "All uploads"
     3431msgstr "Все загрузки"
     3432
     3433#: src/Admin/SettingsPage.php
     3434msgid "Start for all uploads"
     3435msgstr "Запустить для всех загрузок"
     3436
     3437#: src/Admin/SettingsPage.php
     3438msgid "Loading uploads information..."
     3439msgstr "Загружаем данные изображений из uploads..."
     3440
     3441#: src/Admin/SettingsPage.php
     3442msgid "No images were found in uploads for this folder."
     3443msgstr "В этой папке uploads изображения не найдены."
     3444
     3445#: templates/admin-dashboard.php
     3446msgid "Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created."
     3447msgstr "Выберите все изображения из uploads, папку месяца (YYYY/MM) или папку активной темы. Оригиналы не изменяются — создаются только WebP-деривативы."
     3448
     3449#: templates/admin-avif.php
     3450msgid "Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created."
     3451msgstr "Выберите все изображения из uploads, папку месяца (YYYY/MM) или папку активной темы. Оригиналы не изменяются — создаются только AVIF-деривативы."
     3452
     3453#: src/Admin/SettingsPage.php
     3454#, php-format
     3455msgid "Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d."
     3456msgstr "Вне папок по месяцам (входит в «Все загрузки»): %1$d изображений; обработано: %2$d; WebP: %3$d; неудачно: %4$d; осталось: %5$d."
     3457
     3458#: templates/generation-concurrency.php
     3459#, php-format
     3460msgid "Generation of %s is already running. Generating WebP and AVIF at the same time may significantly slow down image processing. You can start both formats or wait until the current run finishes."
     3461msgstr "Сейчас запущена генерация %s. Одновременная генерация WebP и AVIF может существенно замедлить обработку изображений. Можно запустить оба формата или дождаться завершения."
     3462
     3463#: src/Admin/SettingsPage.php
     3464msgid "Loading section..."
     3465msgstr "Загружаем раздел…"
     3466
     3467#: src/Admin/SettingsPage.php
     3468msgid "Sending request..."
     3469msgstr "Отправляем запрос…"
     3470
     3471#: src/Admin/SettingsPage.php
     3472msgid "Could not load the section. The current page is still available."
     3473msgstr "Не удалось загрузить раздел. Текущая страница остаётся доступной."
     3474
     3475#: src/Admin/SettingsPage.php
     3476msgid "The server response could not be confirmed. Open the page to check the result before trying again."
     3477msgstr "Не удалось подтвердить ответ сервера. Откройте страницу и проверьте результат перед повторной попыткой."
     3478
     3479#: src/Admin/SettingsPage.php
     3480msgid "Open page"
     3481msgstr "Открыть страницу"
     3482
     3483#: src/Admin/SettingsPage.php
     3484msgid "There are unsaved settings. Leave this section without saving?"
     3485msgstr "Есть несохранённые настройки. Перейти в другой раздел без сохранения?"
     3486
     3487#: templates/generation-concurrency.php
     3488msgid "Dismiss this notice."
     3489msgstr "Закрыть уведомление."
  • veloimg/trunk/readme.txt

    r3728637 r3739629  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.9.6
     7Stable tag: 0.9.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2525* Original JPG and PNG files remain untouched.
    2626* Safe delivery chains with AVIF, WebP, and original-image fallback.
    27 * Bulk generation for the full Media Library, monthly uploads folders, and selected theme folders.
     27* Bulk generation for all uploads, monthly uploads folders, and selected theme folders.
    2828* Pause and continue long generation runs.
    2929* Separate WebP and AVIF processing with clear statistics and failed-file reasons.
    … …  
    4646* Folder-level statistics, file lists, ZIP download for selected uploads folders, and safe deletion of generated derivatives.
    4747* Per-format optimization summaries for uploads and active/parent theme images; files without a usable derivative retain their original weight in the totals.
     48* Compact main dashboard with frontend delivery and format priority controls, collapsible setup checks, diagnostics, and a page delivery check.
     49* Navigation and settings/test submissions without a full-page reload, with native WordPress fallbacks.
     50* Contextual retry of eligible unsuccessful WebP files with run-local quality and an available encoder; original images, successful files, and saved main settings are preserved.
    4851
    4952VeloIMG is designed for sites with many images. It avoids modifying originals and does not serve generated files when the derivative is invalid, outdated, or larger than the original where that rule applies.
    … …  
    107110= Can WebP and AVIF generation run at the same time? =
    108111
    109 Yes. WebP and AVIF use separate generation flows. On limited shared hosting, running them one after another is recommended to reduce server load.
     112Yes. WebP and AVIF use separate generation flows, but running both at once can slow image processing. The generation screen explains this before starting the second format; the notice can be dismissed for the current generation.
     113
     114= Can I retry unsuccessful WebP files with different settings? =
     115
     116Yes. When eligible unsuccessful files exist in the selected uploads or theme scope, a retry block appears below generation. Choose retry quality and, when both are available, Imagick or GD. This retry uses lossy compression, including PNG. Original images, successful WebP files, and the main generation settings are not changed. Missing or unreadable source images cannot be repaired by changing quality.
    110117
    111118= Does VeloIMG delete generated files? =
    … …  
    116123
    1171241. Main VeloIMG dashboard with server readiness, frontend delivery, format priority, and setup checks.
    118 2. WebP section with generation settings, folder selection, progress, statistics, and file management.
    119 3. AVIF section with generation settings, folder selection, progress, statistics, and file management.
     1252. WebP generation with uploads/theme selection, progress, optimization summary, and file management.
     1263. AVIF generation with uploads/theme selection, progress, optimization summary, and file management.
    1201274. Frontend delivery check showing page response, format coverage, derivative URLs, and unavailable files.
    121 5. Folder statistics with processed, successful, skipped, and unsuccessful images.
     1285. Folder statistics with an expanded file list, status, original and WebP sizes, and savings.
    122129
    123130== Changelog ==
     131
     132= 0.9.7 =
     133
     134* Updated the admin interface with a warm, readable visual system and consistent WebP/AVIF controls, status badges, and notices.
     135* Moved frontend delivery and format priority controls into the main dashboard, with Save settings directly below them and AVIF-first defaults for new or missing choices; explicit saved choices are preserved.
     136* Added collapsible setup, diagnostics, and frontend URL-check blocks on the main page. WebP/AVIF trial reports remain independently available after navigation, with a copyable delivery-bypass parameter and an explained log setting.
     137* Added contextual retry of eligible unsuccessful WebP files with separate quality and available Imagick/GD selection, without changing originals, successful files, or main settings.
     138* Added navigation and settings/test submissions without a full-page reload, retaining native WordPress forms, URL history, and safe recovery when requests fail.
     139* Improved completion-state refresh on the main dashboard and dimmed saved optimization figures while generation or statistics refresh is in progress.
     140* Renamed the whole-upload scope to All uploads and added a dismissible warning that concurrent WebP/AVIF generation can slow image processing.
     141* Prevented setup/generation panels flashing together on first render, stabilized navigation loading notices, and corrected the reported localization, nonce, input sanitation, and output-escaping warnings.
     142* Updated the bundled Russian translation and the English/Russian screenshots. Existing generation settings and original files remain unchanged.
    124143
    125144= 0.9.6 =
    … …  
    188207== Upgrade Notice ==
    189208
     209= 0.9.7 =
     210
     211Updates the dashboard, diagnostics, WebP retry controls, and navigation. Stop both bulk generators before updating. Existing settings and original images are preserved; clear cached admin assets if the old interface remains visible.
     212
    190213= 0.9.6 =
    191214
  • veloimg/trunk/src/Admin/OptimizationSummary.php

    r3728637 r3739629  
    1010                if ( ! wp_verify_nonce( $nonce, 'velowp_home_status' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'veloimg' ) ), 400 ); }
    1111                $cards = array();
     12                $phases = array();
     13                $generation_ids = array();
     14                $pending_refresh = array();
    1215                foreach ( array( 'webp', 'avif' ) as $format ) {
    1316                        $context = self::home_context( $format );
    1417                        // A read failure is not an empty run: the browser retains its previous card.
    15                         if ( $context['available'] ) { $cards[ $format ] = self::home_html( $format, $context ); }
     18                        if ( $context['available'] ) {
     19                                $cards[ $format ] = self::home_html( $format, $context );
     20                                $phases[ $format ] = $context['phase'];
     21                                $generation_ids[ $format ] = $context['generation_id'];
     22                                if ( $context['statistics_pending'] ) { $pending_refresh[ $format ] = $context['run_key']; }
     23                        }
    1624                }
    17                 wp_send_json_success( array( 'cards' => $cards ) );
     25                wp_send_json_success( array( 'cards' => $cards, 'generation_phases' => $phases, 'generation_ids' => $generation_ids, 'pending_refresh' => $pending_refresh ) );
    1826        }
    1927
    … …  
    2836        /** The same saved format summary as generation pages; only status is live. */
    2937        public static function home_context( string $format ) : array {
    30                 $context = array( 'global' => self::read( $format ), 'phase' => 'idle', 'available' => true, 'statistics_pending' => false );
     38                $context = array( 'global' => self::read( $format ), 'phase' => 'idle', 'available' => true, 'statistics_pending' => false, 'run_key' => '', 'generation_id' => '' );
    3139                try {
    3240                        $run = self::home_run( $format );
    3341                        $context['phase'] = $run['phase'];
     42                        $context['run_key'] = $run['key'];
     43                        $context['generation_id'] = $run['id'];
    3444                        // A terminal status is not proof that its ordinary statistics refresh finished.
    3545                        $context['statistics_pending'] = in_array( $run['phase'], array( 'paused', 'completed' ), true )
    … …  
    4858                } else {
    4959                        $state = self::fresh_home_option( 'velowp_avif_mass_state_v2' );
    50                         if ( null === $state || array() === $state ) { return array( 'phase' => 'idle', 'key' => self::home_run_key( $format, array() ) ); }
     60                        if ( null === $state || array() === $state ) { return array( 'phase' => 'idle', 'key' => self::home_run_key( $format, array() ), 'id' => '' ); }
    5161                        if ( ! is_array( $state ) ) { throw new \UnexpectedValueException( 'home_state_invalid' ); }
    5262                        $phase = ! empty( $state['runtime_failure'] ) ? 'failed' : ( ! empty( $state['stopped'] ) ? 'paused' : ( ! empty( $state['done'] ) ? 'completed' : ( ! empty( $state['active'] ) ? 'running' : 'paused' ) ) );
    … …  
    5565                }
    5666                if ( ! in_array( $phase, array( 'idle', 'preparing', 'running', 'stopping', 'paused', 'failed', 'completed' ), true ) ) { throw new \UnexpectedValueException( 'home_phase_invalid' ); }
    57                 return array( 'phase' => $phase, 'key' => self::home_run_key( $format, $state ) );
     67                return array( 'phase' => $phase, 'key' => self::home_run_key( $format, $state ), 'id' => self::generation_id( $format, $state ) );
     68        }
     69
     70        /** Display-only dismissal identity: progress and Stop/Continue never change it. */
     71        private static function generation_id( string $format, array $state ) : string {
     72                $id = (string) ( $state[ 'webp' === $format ? 'run_id' : 'manifest_id' ] ?? '' );
     73                if ( '' === $id && ! empty( $state['started_at'] ) ) {
     74                        $id = implode( '|', array( (string) ( $state['source'] ?? '' ), (string) ( $state['folder'] ?? '' ), (string) $state['started_at'] ) );
     75                }
     76                return '' === $id ? '' : hash( 'sha256', $format . "\0" . $id );
    5877        }
    5978
  • veloimg/trunk/src/Admin/SettingsPage.php

    r3728637 r3739629  
    1717use VeloWP\Conversion\WebpUploadsFolderSnapshot;
    1818use VeloWP\Conversion\WebpManifestMassOrchestrator;
     19use VeloWP\Conversion\WebpRetry;
     20use VeloWP\Database\AvifResultsRepository;
    1921use VeloWP\Database\FilesRepository;
    2022use VeloWP\Database\Installer;
    … …  
    4446        private static $uploads_folder_candidates_request_cache = array();
    4547        private static $theme_preview_request_cache = array();
     48        private static $admin_page_configs = array();
    4649
    4750        public static function svg_icon( string $name ) : string {
    … …  
    100103        private static function default_settings() : array {
    101104                return array(
    102                         'image_strategy'          => 'webp_only',
     105                        'image_strategy'          => 'avif_webp',
    103106                        'delivery_method'         => 'off',
    104107                        'bypass_param'            => 'no_veloimg',
    … …  
    154157                        $strategy = $legacy[ $strategy ];
    155158                }
    156                 return array_key_exists( $strategy, self::image_strategies() ) ? $strategy : 'webp_only';
     159                return array_key_exists( $strategy, self::image_strategies() ) ? $strategy : 'avif_webp';
    157160        }
    158161
    … …  
    171174        private static function delivery_settings_signature( array $settings ) : string {
    172175                $payload = array(
    173                         'image_strategy'  => self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'webp_only' ) ),
     176                        'image_strategy'  => self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) ),
    174177                        'delivery_method' => self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) ),
    175178                        'bypass_param'    => self::sanitize_bypass_param( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ),
    … …  
    438441        public static function register() : void {
    439442                self::maybe_sync_settings();
     443                add_action( 'wp_ajax_velowp_admin_page', array( __CLASS__, 'ajax_admin_page' ) );
    440444
    441445                register_setting(
    … …  
    514518                if ( 'home' === sanitize_key( (string) ( $in['_form'] ?? '' ) ) ) {
    515519                        $out = $current;
    516                         $out['image_strategy']  = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'webp_only' ) ) );
     520                        $out['image_strategy']  = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'avif_webp' ) ) );
    517521                        $out['delivery_method'] = self::sanitize_delivery_method( (string) ( $in['delivery_method'] ?? ( $current['delivery_method'] ?? 'off' ) ) );
    518522                        $bp = isset( $in['bypass_param'] ) ? (string) $in['bypass_param'] : (string) ( $current['bypass_param'] ?? 'no_veloimg' );
    … …  
    557561                $out = array();
    558562
    559                 $out['image_strategy']  = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'webp_only' ) ) );
     563                $out['image_strategy']  = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'avif_webp' ) ) );
    560564                $out['delivery_method'] = self::sanitize_delivery_method( (string) ( $in['delivery_method'] ?? ( $current['delivery_method'] ?? 'off' ) ) );
    561565
    … …  
    609613        }
    610614
    611         public static function render_home() : void {
    612                 if ( ! current_user_can( self::CAPABILITY ) ) {
    613                         wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
    614                 }
    615 
    616                 $logo_h_abs = VELOWP_DIR . 'assets/images/velowp-logo-horizontal.png';
    617                 $has_logo_h = is_file( $logo_h_abs );
    618                 $stored_settings = (array) get_option( 'velowp_settings', array() );
    619                 if ( array_key_exists( 'avif_quality', $stored_settings ) ) {
    620                         $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 );
    621                         if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) {
    622                                 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality;
    623                         }
    624                         if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) {
    625                                 $stored_settings['avif_png_quality'] = $legacy_avif_quality;
    626                         }
    627                 }
    628                 $settings = wp_parse_args( $stored_settings, self::default_settings() );
    629                 $report     = Checker::check();
    630                 $avif_caps  = self::detect_avif_caps();
    631                 $self_test_report = self::load_self_test_report();
    632                 $auto_self_test = self::auto_self_test_pending( $settings );
    633 
    634                 $image_strategies = self::image_strategies();
    635                 $image_strategy   = self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'webp_only' ) );
    636                 $delivery_method  = self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) );
    637                 $webp_ready       = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $report['caps']['imagick_webp'] ) || ! empty( $report['caps']['gd_webp'] ) );
    638                 $avif_ready       = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) );
    639                 $store_writable   = ! empty( $report['caps']['store_writable'] );
    640                 $webp_encoders    = array();
    641                 $avif_encoders    = array();
    642                 if ( ! empty( $report['caps']['imagick_webp'] ) ) {
    643                         $webp_encoders[] = 'Imagick';
    644                 }
    645                 if ( ! empty( $report['caps']['gd_webp'] ) ) {
    646                         $webp_encoders[] = 'GD';
    647                 }
    648                 if ( ! empty( $avif_caps['imagick_avif'] ) ) {
    649                         $avif_encoders[] = 'Imagick';
    650                 }
    651                 if ( ! empty( $avif_caps['gd_avif'] ) ) {
    652                         $avif_encoders[] = 'GD';
    653                 }
    654 
    655                 $webp_page_url        = admin_url( 'admin.php?page=' . self::WEBP_SLUG );
    656                 $avif_page_url        = admin_url( 'admin.php?page=' . self::AVIF_SLUG );
    657                 $help_page_url        = admin_url( 'admin.php?page=' . self::HELP_SLUG );
    658                 $delivery_labels      = array(
    659                         'off'      => __( 'Off (no frontend changes)', 'veloimg' ),
    660                         'php_safe' => __( 'PHP Safe', 'veloimg' ),
    661                         'apache'   => __( 'Apache /.htaccess', 'veloimg' ),
    662                 );
    663                 $delivery_status = $delivery_labels[ $delivery_method ] ?? __( 'Off (no frontend changes)', 'veloimg' );
    664 
    665                 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
    666                 $admin_js_ver       = is_file( VELOWP_DIR . 'assets/js/admin.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin.js' ) : VELOWP_VERSION;
    667                 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION;
    668                 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
    669                 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array(), $folder_runtime_ver, true );
    670                 wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true );
    671                 wp_localize_script(
    672                         'velowp-admin',
    673                         'velowpAdmin',
    674                         array(
    675                                 'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
    676                                 'selfTestNonce' => wp_create_nonce( self::SELF_TEST_NONCE_ACTION ),
    677                                 'autoSelfTest'  => $auto_self_test,
    678                                 'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ),
    679                                 'frontendAuditNonce' => wp_create_nonce( self::FRONTEND_AUDIT_NONCE_ACTION ),
    680                                 'i18n'          => array(
    681                                         'ajaxMissing'        => __( 'AJAX URL is missing', 'veloimg' ),
    682                                         'selfTestLoading'    => __( 'Running self-test...', 'veloimg' ),
    683                                         'selfTestFailed'     => __( 'Could not run the self-test.', 'veloimg' ),
    684                                         'selfTestLastCheck'  => __( 'Last check:', 'veloimg' ),
    685                                         'frontendAuditLoading' => __( 'Checking the page...', 'veloimg' ),
    686                                         'frontendAuditFailed'  => __( 'Could not check the page.', 'veloimg' ),
    687                                         'frontendAuditEnterUrl' => __( 'Enter a page URL to check.', 'veloimg' ),
    688                                         'frontendAuditRunAgain' => __( 'Check again', 'veloimg' ),
    689                                         'frontendAuditIssues' => __( 'Details', 'veloimg' ),
    690                                         'frontendAuditNoIssues' => __( 'No delivery issues found.', 'veloimg' ),
    691                                         'frontendAuditBrokenUrls' => __( 'Broken checked URLs', 'veloimg' ),
    692                                         'runAgain'           => __( 'Run again', 'veloimg' ),
    693                                         'checkStatusSuccess' => __( 'Passed', 'veloimg' ),
    694                                         'checkStatusWarning' => __( 'Warning', 'veloimg' ),
    695                                         'checkStatusError'   => __( 'Failed', 'veloimg' ),
    696                                         'checkStatusInfo'    => __( 'Info', 'veloimg' ),
    697                                 ),
    698                         )
    699                 );
    700 
    701                 include VELOWP_DIR . 'templates/admin-home.php';
    702         }
    703 
    704         public static function render_help() : void {
    705                 if ( ! current_user_can( self::CAPABILITY ) ) {
    706                         wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
    707                 }
    708 
    709                 $home_page_url = admin_url( 'admin.php?page=' . self::SLUG );
    710                 $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG );
    711                 $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG );
    712 
    713                 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
    714                 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
    715 
    716                 include VELOWP_DIR . 'templates/admin-help.php';
    717         }
    718 
    719         public static function render_avif() : void {
    720                 if ( ! current_user_can( self::CAPABILITY ) ) {
    721                         wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
    722                 }
    723 
    724                 $stored_settings = (array) get_option( 'velowp_settings', array() );
    725                 if ( array_key_exists( 'avif_quality', $stored_settings ) ) {
    726                         $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 );
    727                         if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) {
    728                                 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality;
    729                         }
    730                         if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) {
    731                                 $stored_settings['avif_png_quality'] = $legacy_avif_quality;
    732                         }
    733                 }
    734                 $settings = wp_parse_args( $stored_settings, self::default_settings() );
    735                 $report       = Checker::check();
    736                 $avif_caps    = self::detect_avif_caps();
    737                 $store        = DerivativesStore::default_store();
    738                 $mapper       = new PathMapper( $store );
    739                 $avif_jpeg_quality = self::sanitize_avif_quality( $settings['avif_jpeg_quality'] ?? ( $settings['avif_quality'] ?? 85 ) );
    740                 $avif_png_quality  = self::sanitize_avif_quality( $settings['avif_png_quality'] ?? ( $settings['avif_quality'] ?? 85 ) );
    741                 $store_ready  = ! empty( $report['caps']['store_writable'] );
    742                 $encoder_ready = ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] );
    743                 $avif_ready   = $store_ready && $encoder_ready;
    744                 $avif_hosting_ready = AvifMass::hosting_readiness();
    745                 $encoder_labels = array();
    746                 $avif_candidates = self::get_large_recent_images( $mapper );
    747                 $avif_quick = array(
    748                         'submitted'    => false,
    749                         'input'        => '',
    750                         'ok'           => false,
    751                         'message'      => '',
    752                         'error'        => '',
    753                         'original_rel' => '',
    754                         'd_expected'   => array(),
    755                         'result'       => array(),
    756                         'verdict'      => array(),
    757                 );
    758 
    759                 if ( ! empty( $avif_caps['imagick_avif'] ) ) {
    760                         $encoder_labels[] = 'Imagick';
    761                 }
    762                 if ( ! empty( $avif_caps['gd_avif'] ) ) {
    763                         $encoder_labels[] = 'GD';
    764                 }
    765 
    766                 $store_base_rel = trim( (string) $store->base_rel(), '/' );
    767                 $uploads_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'YYYY/MM/example.jpg.avif';
    768                 $theme_example_rel   = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'theme/theme-slug/assets/example.png.avif';
    769                 $upload = wp_get_upload_dir();
    770                 $uploads_basedir = (string) ( $upload['basedir'] ?? '' );
    771                 $avif_folder_candidates = self::list_uploads_year_month_candidates( $uploads_basedir );
    772                 $theme_asset_candidates = self::discover_theme_asset_folders();
    773                 $avif_mass_status = AvifMass::status_payload();
    774 
    775                 if ( isset( $_POST['velowp_avif_quick_nonce'] ) && check_admin_referer( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ) ) {
    776                         $avif_quick['submitted'] = true;
    777                         $raw = trim( self::post_text( 'velowp_avif_quick_input' ) );
    778                         $avif_quick['input'] = $raw;
    779 
    780                         $valid = $mapper->validate_original_rel( $raw );
    781                         if ( empty( $valid['ok'] ) ) {
    782                                 $avif_quick['error'] = (string) ( $valid['error'] ?? __( 'Invalid input.', 'veloimg' ) );
    783                         } else {
    784                                 $rel = (string) $valid['rel'];
    785                                 $processor = new AvifProcessor(
    786                                         $store,
    787                                         $mapper,
    788                                         array_merge( (array) $avif_caps, array( 'exif' => function_exists( 'exif_read_data' ) ) ),
    789                                         $settings
    790                                 );
    791                                 $derivative_abs = $processor->derivative_abs_from_original_rel( $rel );
    792                                 $avif_quick['original_rel'] = $rel;
    793                                 $avif_quick['d_expected'] = array(
    794                                         'derivative_rel' => $processor->derivative_rel_from_original_rel( $rel ),
    795                                         'derivative_abs' => $derivative_abs,
    796                                         'derivative_url' => $processor->derivative_url_from_original_rel( $rel ),
    797                                         'exists_before'  => is_file( $derivative_abs ),
    798                                         'exists'         => is_file( $derivative_abs ),
    799                                         'valid'          => is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ),
    800                                 );
    801 
    802                                 $res = $processor->generate_from_rel( $rel, false );
    803                                 $avif_quick['result'] = is_array( $res ) ? $res : array();
    804                                 $avif_quick['ok'] = ! empty( $res['ok'] );
    805                                 $avif_quick['message'] = self::avif_quick_result_message( $avif_quick['result'], $store_ready, $encoder_ready );
    806                                 $avif_quick['d_expected']['exists'] = is_file( $derivative_abs );
    807                                 $avif_quick['d_expected']['valid']  = is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs );
    808                         }
    809 
    810                         $avif_quick['verdict'] = self::build_avif_quick_verdict( $avif_quick, $store_ready, $encoder_ready );
    811                 }
    812 
    813                 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
    814                 $admin_avif_js_ver  = is_file( VELOWP_DIR . 'assets/js/admin-avif.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin-avif.js' ) : VELOWP_VERSION;
    815                 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION;
    816                 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
    817                 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array(), $folder_runtime_ver, true );
    818                 wp_enqueue_script( 'velowp-admin-avif', VELOWP_URL . 'assets/js/admin-avif.js', array( 'velowp-folder-statistics-runtime' ), $admin_avif_js_ver, true );
    819                 wp_localize_script(
    820                         'velowp-admin-avif',
    821                         'velowpAvifAdmin',
    822                         array(
    823                                 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    824                                 'nonce'   => wp_create_nonce( AvifMass::nonce_action() ),
    825                                 'nonceAction' => 'velowp_avif_mass_nonce',
    826                                 'initialMassStatus' => $avif_mass_status,
    827                                 'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'avif' ),
    828                                 'initialGlobalSummary' => OptimizationSummary::read( 'avif' ),
    829                                 'optimizationSummaryAction' => 'velowp_optimization_summary',
    830                                 'folderStatisticsTitles' => array( 'uploads' => __( 'Uploads folder statistics', 'veloimg' ), 'theme' => __( 'Theme folder statistics', 'veloimg' ) ),
    831                                 'i18n'    => array(
    832                                         'ajaxMissing'  => __( 'AJAX URL missing', 'veloimg' ),
    833                                         'chooseFolder' => __( 'Choose a folder.', 'veloimg' ),
    834                                         'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ),
    835                                         'loadingStats' => __( 'Loading folder information...', 'veloimg' ),
    836                                         'loadingAll'   => __( 'Loading Media Library information...', 'veloimg' ),
    837                                         'loadingTheme' => __( 'Loading theme folder information...', 'veloimg' ),
    838                                         'emptyFolder'  => __( 'No images were found in the Media Library for this folder.', 'veloimg' ),
    839                                         'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ),
    840                                         'ready'        => __( 'Ready.', 'veloimg' ),
    841                                         'starting'     => __( 'Starting AVIF generation...', 'veloimg' ),
    842                                         'startingTheme' => __( 'Starting theme folder AVIF processing...', 'veloimg' ),
    843                                         'running'      => __( 'Generating AVIF...', 'veloimg' ),
    844                                         'runningTheme' => __( 'Generating AVIF for the theme folder...', 'veloimg' ),
    845                                         'processingFile' => __( 'Processing file:', 'veloimg' ),
    846                                         'currentStepTime' => __( 'Current step:', 'veloimg' ),
    847                                         'noProgressFor' => __( 'No progress for:', 'veloimg' ),
    848                                         'stoppingDrain' => __( 'Stopping. Finishing the current work...', 'veloimg' ),
    849                                         'secondsShort' => __( 'sec', 'veloimg' ),
    850                                         'minutesShort' => __( 'min', 'veloimg' ),
    851                                         'slowCurrentFile' => __( 'This file is taking longer than usual. If the server stops the request, the file will be marked as unsuccessful and generation will continue.', 'veloimg' ),
    852                                         'timeoutLimit' => __( 'Timeout limit:', 'veloimg' ),
    853                                         'stopping'     => __( 'Stopping...', 'veloimg' ),
    854                                         'stopped'      => __( 'Stopped.', 'veloimg' ),
    855                                         'done'         => __( 'AVIF generation completed.', 'veloimg' ),
    856                                         'doneLabel'    => __( 'Generation completed', 'veloimg' ),
    857                                         'failed'       => __( 'AVIF generation failed.', 'veloimg' ),
    858                                         'errorTitle'   => __( 'Error', 'veloimg' ),
    859                                         'hint'         => __( 'Hint', 'veloimg' ),
    860                                         'longRun'      => __( 'There are many files. Generation may take a while depending on the server. You can stop and continue later without losing progress.', 'veloimg' ),
    861                                         'unsuccessful' => __( 'Unsuccessful', 'veloimg' ),
    862                                         'avifBiggerExplain' => __( 'Some images are marked as "Unsuccessful" because AVIF became larger than the original, so the plugin intentionally does not save it.', 'veloimg' ),
    863                                         'avifWhatToDo' => __( 'If you want to try again, reduce the AVIF JPEG/PNG quality and run generation again.', 'veloimg' ),
    864                                         'unsuccessfulReasonsInline' => __( 'Unsuccessful items were processed, but the derivative was not saved. Reasons:', 'veloimg' ),
    865                                         'skippedReasonsInline' => __( 'There are "Unsuccessful" items. The main reasons are listed here:', 'veloimg' ),
    866                                         'skippedDetailsBelow' => __( 'There are "Unsuccessful" items. See the statistics for details.', 'veloimg' ),
    867                                         'whyNoAvif'    => __( 'Why items are still without AVIF', 'veloimg' ),
    868                                         'ajaxFailed'   => __( 'AJAX request failed', 'veloimg' ),
    869                                         'nonceRefreshFailed' => __( 'Could not refresh the security token. Reload the VeloIMG page.', 'veloimg' ),
    870                                         'nonceExpired' => __( 'The page session expired. Refreshing the security token...', 'veloimg' ),
    871                                         'requestTimeout' => __( 'The server took too long to respond. This usually means the hosting environment limited the request. Try again; progress is not lost.', 'veloimg' ),
    872                                         'requestRetrying' => __( 'The server did not answer in time. Retrying; progress is not lost.', 'veloimg' ),
    873                                         'startWaiting' => __( 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.', 'veloimg' ),
    874                                         'staleRun' => __( 'This AVIF run was replaced. Refresh the status before continuing.', 'veloimg' ),
    875                                         'networkOffline' => __( 'Network connection appears to be offline.', 'veloimg' ),
    876                                         'networkChanged' => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ),
    877                                         'locked'       => __( 'Another generation process is running. Wait until it finishes.', 'veloimg' ),
    878                                         'noDataYet'    => __( 'No data yet.', 'veloimg' ),
    879                                         'processed'    => __( 'Processed:', 'veloimg' ),
    880                                         'startAvif'    => __( 'Start AVIF generation', 'veloimg' ),
    881                                         'startAll'     => __( 'Start for the entire Media Library', 'veloimg' ),
    882                                         'startFolder'  => __( 'Start AVIF generation', 'veloimg' ),
    883                                         'startTheme'   => __( 'Start for the theme folder', 'veloimg' ),
    884                                         'continueRun'  => __( 'Continue', 'veloimg' ),
    885                                         'imagesLabel'  => __( 'Images', 'veloimg' ),
    886                                         'left'         => __( 'Left', 'veloimg' ),
    887                                         'foldersEmpty' => __( 'No data.', 'veloimg' ),
    888                                         'foldersFailed' => __( 'Could not load folder overview.', 'veloimg' ),
    889                                         'statsFrozen' => __( 'Generation is running. Statistics will be refreshed after completion or a confirmed stop.', 'veloimg' ),
    890                                         /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */
    891                                         'outsideMonthsSummary' => str_replace( 'WebP', 'AVIF', __( 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ) ),
    892                                         'savedLine'    => __( 'Savings', 'veloimg' ),
    893                                         'active'       => __( 'In progress', 'veloimg' ),
    894                                         'paused'       => __( 'Paused', 'veloimg' ),
    895                                         'completed'    => __( 'Completed', 'veloimg' ),
    896                                         'deleting'     => __( 'Deleting AVIF...', 'veloimg' ),
    897                                         'deletingTheme'=> __( 'Deleting AVIF files for the theme folder...', 'veloimg' ),
    898                                         'deleteFailed' => __( 'Could not delete AVIF.', 'veloimg' ),
    899                                         'deleted'      => __( 'AVIF files were deleted.', 'veloimg' ),
    900                                         'deletedHint'  => __( 'You can run generation again.', 'veloimg' ),
    901                                         'deleteConfirm'=> __( 'Delete all AVIF files in the selected folder? Original files will not be touched.', 'veloimg' ),
    902                                         'deleteThemeConfirm' => __( 'Delete all AVIF files for the selected theme folder? Original files will not be touched.', 'veloimg' ),
    903                                         'deleteAllConfirm' => __( 'Delete ALL generated AVIF files across the Media Library and theme derivatives? Original files will not be touched.', 'veloimg' ),
    904                                         'noFolderData' => __( 'No file data available.', 'veloimg' ),
    905                                         'open'         => __( 'Open', 'veloimg' ),
    906                                         'avif'         => __( 'AVIF', 'veloimg' ),
    907                                         'file'         => __( 'File', 'veloimg' ),
    908                                         'status'       => __( 'Status', 'veloimg' ),
    909                                         'origSize'     => __( 'Original', 'veloimg' ),
    910                                         'saving'       => __( 'Savings', 'veloimg' ),
    911                                         'loadedCount'  => __( 'loaded', 'veloimg' ),
    912                                         'loadingMore'  => __( 'Loading...', 'veloimg' ),
    913                                         'scrollToLoadMore' => __( 'Scroll down to load more...', 'veloimg' ),
    914                                         'allLoaded'    => __( 'All rows are loaded.', 'veloimg' ),
    915                                 ),
    916                         )
    917                 );
    918 
    919                 include VELOWP_DIR . 'templates/admin-avif.php';
    920         }
    921 
    922         public static function render() : void {
    923                 if ( ! current_user_can( self::CAPABILITY ) ) {
    924                         wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
    925                 }
    926 
    927                 $active_panel = 'mass';
    928                 if ( isset( $_GET['settings-updated'] ) ) {
    929                         $active_panel = 'setup';
    930                 }
    931 
    932                 $report   = Checker::check();
    933                 $settings = wp_parse_args( (array) get_option( 'velowp_settings', array() ), self::default_settings() );
    934                 $theme_asset_candidates = self::discover_theme_asset_folders();
    935                 $theme_asset_selected   = isset( $settings['theme_asset_folders'] ) && is_array( $settings['theme_asset_folders'] ) ? array_values( array_unique( array_map( 'strval', (array) $settings['theme_asset_folders'] ) ) ) : array();
    936 
    937                 $store  = DerivativesStore::default_store();
    938                 $mapper = new PathMapper( $store );
    939                 $repo   = new QueueRepository();
    940 
     615        private static function home_webp_check( DerivativesStore $store, PathMapper $mapper, array $report, array $settings ) : array {
     616                $repo = new QueueRepository();
    941617                $quick = array(
    942618                        'submitted'  => false,
    … …  
    953629                );
    954630
    955                 $candidates = self::get_large_recent_images( $mapper );
    956 
    957631                if ( isset( $_POST['velowp_quick_nonce'] ) && check_admin_referer( 'velowp_quick', 'velowp_quick_nonce' ) ) {
    958632                        $quick['submitted'] = true;
    959                         $active_panel = 'quick';
    960633                        $quick['action']    = self::post_key( 'velowp_quick_action' );
    961634                        $raw               = trim( self::post_text( 'velowp_quick_input' ) );
    … …  
    1037710                        }
    1038711                }
     712                return self::remember_home_trial( 'webp', $quick );
     713        }
     714
     715        private static function home_avif_check( DerivativesStore $store, PathMapper $mapper, array $avif_caps, array $settings, bool $store_ready, bool $encoder_ready ) : array {
     716                $avif_quick = array(
     717                        'submitted'    => false,
     718                        'input'        => '',
     719                        'ok'           => false,
     720                        'message'      => '',
     721                        'error'        => '',
     722                        'original_rel' => '',
     723                        'd_expected'   => array(),
     724                        'result'       => array(),
     725                        'verdict'      => array(),
     726                );
     727
     728                if ( isset( $_POST['velowp_avif_quick_nonce'] ) && check_admin_referer( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ) ) {
     729                        $avif_quick['submitted'] = true;
     730                        $raw = trim( self::post_text( 'velowp_avif_quick_input' ) );
     731                        $avif_quick['input'] = $raw;
     732
     733                        $valid = $mapper->validate_original_rel( $raw );
     734                        if ( empty( $valid['ok'] ) ) {
     735                                $avif_quick['error'] = (string) ( $valid['error'] ?? __( 'Invalid input.', 'veloimg' ) );
     736                        } else {
     737                                $rel = (string) $valid['rel'];
     738                                $processor = new AvifProcessor(
     739                                        $store,
     740                                        $mapper,
     741                                        array_merge( (array) $avif_caps, array( 'exif' => function_exists( 'exif_read_data' ) ) ),
     742                                        $settings
     743                                );
     744                                $derivative_abs = $processor->derivative_abs_from_original_rel( $rel );
     745                                $avif_quick['original_rel'] = $rel;
     746                                $avif_quick['d_expected'] = array(
     747                                        'derivative_rel' => $processor->derivative_rel_from_original_rel( $rel ),
     748                                        'derivative_abs' => $derivative_abs,
     749                                        'derivative_url' => $processor->derivative_url_from_original_rel( $rel ),
     750                                        'exists_before'  => is_file( $derivative_abs ),
     751                                        'exists'         => is_file( $derivative_abs ),
     752                                        'valid'          => is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ),
     753                                );
     754
     755                                $res = $processor->generate_from_rel( $rel, false );
     756                                $avif_quick['result'] = is_array( $res ) ? $res : array();
     757                                $avif_quick['ok'] = ! empty( $res['ok'] );
     758                                $avif_quick['message'] = self::avif_quick_result_message( $avif_quick['result'], $store_ready, $encoder_ready );
     759                                $avif_quick['d_expected']['exists'] = is_file( $derivative_abs );
     760                                $avif_quick['d_expected']['valid']  = is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs );
     761                        }
     762
     763                        $avif_quick['verdict'] = self::build_avif_quick_verdict( $avif_quick, $store_ready, $encoder_ready );
     764                }
     765                return self::remember_home_trial( 'avif', $avif_quick );
     766        }
     767
     768        /** Keep one last authenticated trial per format, scoped to this WordPress site. */
     769        private static function remember_home_trial( string $format, array $quick ) : array {
     770                if ( ! in_array( $format, array( 'webp', 'avif' ), true ) ) {
     771                        return $quick;
     772                }
     773                $key = 'velowp_last_trial_' . $format;
     774                if ( ! empty( $quick['submitted'] ) ) {
     775                        $snapshot = $quick;
     776                        // The selected result is sufficient; do not persist unrelated queue items.
     777                        unset( $snapshot['tick'] );
     778                        update_option( $key, $snapshot, false );
     779                        return $quick;
     780                }
     781                $stored = get_option( $key, array() );
     782                return is_array( $stored ) && ! empty( $stored['submitted'] )
     783                        ? array_replace( $quick, array_intersect_key( $stored, $quick ) )
     784                        : $quick;
     785        }
     786
     787        public static function render_home() : void {
     788                if ( ! current_user_can( self::CAPABILITY ) ) {
     789                        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
     790                }
     791
     792                $logo_h_abs = VELOWP_DIR . 'assets/images/velowp-logo-horizontal.png';
     793                $has_logo_h = is_file( $logo_h_abs );
     794                $stored_settings = (array) get_option( 'velowp_settings', array() );
     795                if ( array_key_exists( 'avif_quality', $stored_settings ) ) {
     796                        $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 );
     797                        if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) {
     798                                $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality;
     799                        }
     800                        if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) {
     801                                $stored_settings['avif_png_quality'] = $legacy_avif_quality;
     802                        }
     803                }
     804                $settings = wp_parse_args( $stored_settings, self::default_settings() );
     805                $report     = Checker::check();
     806                $avif_caps  = self::detect_avif_caps();
     807                $self_test_report = self::load_self_test_report();
     808                $auto_self_test = self::auto_self_test_pending( $settings );
     809
     810                $image_strategies = self::image_strategies();
     811                $image_strategy   = self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) );
     812                $delivery_method  = self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) );
     813                $webp_ready       = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $report['caps']['imagick_webp'] ) || ! empty( $report['caps']['gd_webp'] ) );
     814                $avif_ready       = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) );
     815                $store_writable   = ! empty( $report['caps']['store_writable'] );
     816                $store            = DerivativesStore::default_store();
     817                $mapper           = new PathMapper( $store );
     818                $recent_images    = self::get_large_recent_images( $mapper );
     819                $quick            = self::home_webp_check( $store, $mapper, $report, $settings );
     820                $avif_quick       = self::home_avif_check( $store, $mapper, $avif_caps, $settings, $store_writable, ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) );
     821                $candidates       = self::home_trial_candidates( $recent_images, $mapper, 'webp', $quick, $settings );
     822                $avif_candidates  = self::home_trial_candidates( $recent_images, $mapper, 'avif', $avif_quick, $settings );
     823                $diagnostics_open = wp_verify_nonce( self::post_text( 'velowp_quick_nonce' ), 'velowp_quick' )
     824                        || wp_verify_nonce( self::post_text( 'velowp_avif_quick_nonce' ), 'velowp_avif_quick' );
     825                $bypass_query     = '?' . self::sanitize_bypass_param( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ) . '=1';
     826                $webp_encoders    = array();
     827                $avif_encoders    = array();
     828                if ( ! empty( $report['caps']['imagick_webp'] ) ) {
     829                        $webp_encoders[] = 'Imagick';
     830                }
     831                if ( ! empty( $report['caps']['gd_webp'] ) ) {
     832                        $webp_encoders[] = 'GD';
     833                }
     834                if ( ! empty( $avif_caps['imagick_avif'] ) ) {
     835                        $avif_encoders[] = 'Imagick';
     836                }
     837                if ( ! empty( $avif_caps['gd_avif'] ) ) {
     838                        $avif_encoders[] = 'GD';
     839                }
     840
     841                $webp_page_url        = admin_url( 'admin.php?page=' . self::WEBP_SLUG );
     842                $avif_page_url        = admin_url( 'admin.php?page=' . self::AVIF_SLUG );
     843                $help_page_url        = admin_url( 'admin.php?page=' . self::HELP_SLUG );
     844                $delivery_labels      = array(
     845                        'off'      => __( 'Off (no frontend changes)', 'veloimg' ),
     846                        'php_safe' => __( 'PHP Safe', 'veloimg' ),
     847                        'apache'   => __( 'Apache /.htaccess', 'veloimg' ),
     848                );
     849                $delivery_status = $delivery_labels[ $delivery_method ] ?? __( 'Off (no frontend changes)', 'veloimg' );
     850
     851                $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
     852                $admin_js_ver       = is_file( VELOWP_DIR . 'assets/js/admin.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin.js' ) : VELOWP_VERSION;
     853                $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION;
     854                wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
     855                wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true );
     856                wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true );
     857                self::localize_admin_script(
     858                        'velowp-admin',
     859                        'velowpAdmin',
     860                        array(
     861                                'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
     862                                'selfTestNonce' => wp_create_nonce( self::SELF_TEST_NONCE_ACTION ),
     863                                'autoSelfTest'  => $auto_self_test,
     864                                'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ),
     865                                'optimizationSummaryAction' => 'velowp_optimization_summary',
     866                                'homeSummaryNonces' => array( 'webp' => wp_create_nonce( self::MASS_NONCE_ACTION ), 'avif' => wp_create_nonce( AvifMass::nonce_action() ) ),
     867                                'frontendAuditNonce' => wp_create_nonce( self::FRONTEND_AUDIT_NONCE_ACTION ),
     868                                'i18n'          => array(
     869                                        'selectImage'        => __( 'Select an image', 'veloimg' ),
     870                                        'ajaxMissing'        => __( 'AJAX URL is missing', 'veloimg' ),
     871                                        'selfTestLoading'    => __( 'Running self-test...', 'veloimg' ),
     872                                        'selfTestFailed'     => __( 'Could not run the self-test.', 'veloimg' ),
     873                                        'selfTestLastCheck'  => __( 'Last check:', 'veloimg' ),
     874                                        'frontendAuditLoading' => __( 'Checking the page...', 'veloimg' ),
     875                                        'frontendAuditFailed'  => __( 'Could not check the page.', 'veloimg' ),
     876                                        'frontendAuditEnterUrl' => __( 'Enter a page URL to check.', 'veloimg' ),
     877                                        'frontendAuditRunAgain' => __( 'Check again', 'veloimg' ),
     878                                        'frontendAuditIssues' => __( 'Details', 'veloimg' ),
     879                                        'frontendAuditNoIssues' => __( 'No delivery issues found.', 'veloimg' ),
     880                                        'frontendAuditBrokenUrls' => __( 'Broken checked URLs', 'veloimg' ),
     881                                        'runAgain'           => __( 'Run again', 'veloimg' ),
     882                                        'checkStatusSuccess' => __( 'Passed', 'veloimg' ),
     883                                        'checkStatusWarning' => __( 'Warning', 'veloimg' ),
     884                                        'checkStatusError'   => __( 'Failed', 'veloimg' ),
     885                                        'checkStatusInfo'    => __( 'Info', 'veloimg' ),
     886                                ),
     887                        )
     888                );
     889
     890                include VELOWP_DIR . 'templates/admin-home.php';
     891                self::render_navigation_config( self::SLUG );
     892        }
     893
     894        public static function render_help() : void {
     895                if ( ! current_user_can( self::CAPABILITY ) ) {
     896                        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
     897                }
     898
     899                $home_page_url = admin_url( 'admin.php?page=' . self::SLUG );
     900                $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG );
     901                $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG );
     902
     903                $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
     904                wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
     905
     906                include VELOWP_DIR . 'templates/admin-help.php';
     907                self::render_navigation_config( self::HELP_SLUG );
     908        }
     909
     910        public static function render_avif() : void {
     911                if ( ! current_user_can( self::CAPABILITY ) ) {
     912                        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
     913                }
     914
     915                $stored_settings = (array) get_option( 'velowp_settings', array() );
     916                if ( array_key_exists( 'avif_quality', $stored_settings ) ) {
     917                        $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 );
     918                        if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) {
     919                                $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality;
     920                        }
     921                        if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) {
     922                                $stored_settings['avif_png_quality'] = $legacy_avif_quality;
     923                        }
     924                }
     925                $settings = wp_parse_args( $stored_settings, self::default_settings() );
     926                $report       = Checker::check();
     927                $avif_caps    = self::detect_avif_caps();
     928                $store        = DerivativesStore::default_store();
     929                $mapper       = new PathMapper( $store );
     930                $avif_jpeg_quality = self::sanitize_avif_quality( $settings['avif_jpeg_quality'] ?? ( $settings['avif_quality'] ?? 85 ) );
     931                $avif_png_quality  = self::sanitize_avif_quality( $settings['avif_png_quality'] ?? ( $settings['avif_quality'] ?? 85 ) );
     932                $store_ready  = ! empty( $report['caps']['store_writable'] );
     933                $encoder_ready = ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] );
     934                $avif_ready   = $store_ready && $encoder_ready;
     935                $avif_hosting_ready = AvifMass::hosting_readiness();
     936                $encoder_labels = array();
     937
     938                if ( ! empty( $avif_caps['imagick_avif'] ) ) {
     939                        $encoder_labels[] = 'Imagick';
     940                }
     941                if ( ! empty( $avif_caps['gd_avif'] ) ) {
     942                        $encoder_labels[] = 'GD';
     943                }
     944
     945                $store_base_rel = trim( (string) $store->base_rel(), '/' );
     946                $uploads_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'YYYY/MM/example.jpg.avif';
     947                $theme_example_rel   = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'theme/theme-slug/assets/example.png.avif';
     948                $upload = wp_get_upload_dir();
     949                $uploads_basedir = (string) ( $upload['basedir'] ?? '' );
     950                $avif_folder_candidates = self::list_uploads_year_month_candidates( $uploads_basedir );
     951                $theme_asset_candidates = self::discover_theme_asset_folders();
     952                $avif_mass_status = AvifMass::status_payload();
     953
     954
     955                $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION;
     956                $admin_avif_js_ver  = is_file( VELOWP_DIR . 'assets/js/admin-avif.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin-avif.js' ) : VELOWP_VERSION;
     957                $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION;
     958                wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
     959                wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true );
     960                wp_enqueue_script( 'velowp-admin-avif', VELOWP_URL . 'assets/js/admin-avif.js', array( 'velowp-folder-statistics-runtime' ), $admin_avif_js_ver, true );
     961                self::localize_admin_script(
     962                        'velowp-admin-avif',
     963                        'velowpAvifAdmin',
     964                        array(
     965                                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     966                                'nonce'   => wp_create_nonce( AvifMass::nonce_action() ),
     967                                'nonceAction' => 'velowp_avif_mass_nonce',
     968                                'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ),
     969                                'initialMassStatus' => $avif_mass_status,
     970                                'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'avif' ),
     971                                'initialGlobalSummary' => OptimizationSummary::read( 'avif' ),
     972                                'optimizationSummaryAction' => 'velowp_optimization_summary',
     973                                'folderStatisticsTitles' => array( 'uploads' => __( 'Uploads folder statistics', 'veloimg' ), 'theme' => __( 'Theme folder statistics', 'veloimg' ) ),
     974                                'i18n'    => array(
     975                                        'ajaxMissing'  => __( 'AJAX URL missing', 'veloimg' ),
     976                                        'chooseFolder' => __( 'Choose a folder.', 'veloimg' ),
     977                                        'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ),
     978                                        'loadingStats' => __( 'Loading folder information...', 'veloimg' ),
     979                                        'loadingAll'   => __( 'Loading uploads information...', 'veloimg' ),
     980                                        'loadingTheme' => __( 'Loading theme folder information...', 'veloimg' ),
     981                                        'emptyFolder'  => __( 'No images were found in uploads for this folder.', 'veloimg' ),
     982                                        'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ),
     983                                        'ready'        => __( 'Ready.', 'veloimg' ),
     984                                        'starting'     => __( 'Starting AVIF generation...', 'veloimg' ),
     985                                        'startingTheme' => __( 'Starting theme folder AVIF processing...', 'veloimg' ),
     986                                        'running'      => __( 'Generating AVIF...', 'veloimg' ),
     987                                        'runningTheme' => __( 'Generating AVIF for the theme folder...', 'veloimg' ),
     988                                        'processingFile' => __( 'Processing file:', 'veloimg' ),
     989                                        'currentStepTime' => __( 'Current step:', 'veloimg' ),
     990                                        'noProgressFor' => __( 'No progress for:', 'veloimg' ),
     991                                        'stoppingDrain' => __( 'Stopping. Finishing the current work...', 'veloimg' ),
     992                                        'secondsShort' => __( 'sec', 'veloimg' ),
     993                                        'minutesShort' => __( 'min', 'veloimg' ),
     994                                        'slowCurrentFile' => __( 'This file is taking longer than usual. If the server stops the request, the file will be marked as unsuccessful and generation will continue.', 'veloimg' ),
     995                                        'timeoutLimit' => __( 'Timeout limit:', 'veloimg' ),
     996                                        'stopping'     => __( 'Stopping...', 'veloimg' ),
     997                                        'stopped'      => __( 'Stopped.', 'veloimg' ),
     998                                        'done'         => __( 'AVIF generation completed.', 'veloimg' ),
     999                                        'doneLabel'    => __( 'Generation completed', 'veloimg' ),
     1000                                        'failed'       => __( 'AVIF generation failed.', 'veloimg' ),
     1001                                        'errorTitle'   => __( 'Error', 'veloimg' ),
     1002                                        'hint'         => __( 'Hint', 'veloimg' ),
     1003                                        'longRun'      => __( 'There are many files. Generation may take a while depending on the server. You can stop and continue later without losing progress.', 'veloimg' ),
     1004                                        'unsuccessful' => __( 'Unsuccessful', 'veloimg' ),
     1005                                        'avifBiggerExplain' => __( 'Some images are marked as "Unsuccessful" because AVIF became larger than the original, so the plugin intentionally does not save it.', 'veloimg' ),
     1006                                        'avifWhatToDo' => __( 'If you want to try again, reduce the AVIF JPEG/PNG quality and run generation again.', 'veloimg' ),
     1007                                        'unsuccessfulReasonsInline' => __( 'Unsuccessful items were processed, but the derivative was not saved. Reasons:', 'veloimg' ),
     1008                                        'skippedReasonsInline' => __( 'There are "Unsuccessful" items. The main reasons are listed here:', 'veloimg' ),
     1009                                        'skippedDetailsBelow' => __( 'There are "Unsuccessful" items. See the statistics for details.', 'veloimg' ),
     1010                                        'whyNoAvif'    => __( 'Why items are still without AVIF', 'veloimg' ),
     1011                                        'ajaxFailed'   => __( 'AJAX request failed', 'veloimg' ),
     1012                                        'nonceRefreshFailed' => __( 'Could not refresh the security token. Reload the VeloIMG page.', 'veloimg' ),
     1013                                        'nonceExpired' => __( 'The page session expired. Refreshing the security token...', 'veloimg' ),
     1014                                        'requestTimeout' => __( 'The server took too long to respond. This usually means the hosting environment limited the request. Try again; progress is not lost.', 'veloimg' ),
     1015                                        'requestRetrying' => __( 'The server did not answer in time. Retrying; progress is not lost.', 'veloimg' ),
     1016                                        'startWaiting' => __( 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.', 'veloimg' ),
     1017                                        'staleRun' => __( 'This AVIF run was replaced. Refresh the status before continuing.', 'veloimg' ),
     1018                                        'networkOffline' => __( 'Network connection appears to be offline.', 'veloimg' ),
     1019                                        'networkChanged' => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ),
     1020                                        'locked'       => __( 'Another generation process is running. Wait until it finishes.', 'veloimg' ),
     1021                                        'noDataYet'    => __( 'No data yet.', 'veloimg' ),
     1022                                        'processed'    => __( 'Processed:', 'veloimg' ),
     1023                                        'startAvif'    => __( 'Start AVIF generation', 'veloimg' ),
     1024                                        'startAll'     => __( 'Start for all uploads', 'veloimg' ),
     1025                                        'startFolder'  => __( 'Start AVIF generation', 'veloimg' ),
     1026                                        'startTheme'   => __( 'Start for the theme folder', 'veloimg' ),
     1027                                        'continueRun'  => __( 'Continue', 'veloimg' ),
     1028                                        'imagesLabel'  => __( 'Images', 'veloimg' ),
     1029                                        'left'         => __( 'Left', 'veloimg' ),
     1030                                        'foldersEmpty' => __( 'No data.', 'veloimg' ),
     1031                                        'foldersFailed' => __( 'Could not load folder overview.', 'veloimg' ),
     1032                                        'statsFrozen' => __( 'Generation is running. Statistics will be refreshed after completion or a confirmed stop.', 'veloimg' ),
     1033                                        /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */
     1034                                        'outsideMonthsSummary' => str_replace( 'WebP', 'AVIF', __( 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ) ),
     1035                                        'savedLine'    => __( 'Savings', 'veloimg' ),
     1036                                        'active'       => __( 'In progress', 'veloimg' ),
     1037                                        'paused'       => __( 'Paused', 'veloimg' ),
     1038                                        'completed'    => __( 'Completed', 'veloimg' ),
     1039                                        'deleting'     => __( 'Deleting AVIF...', 'veloimg' ),
     1040                                        'deletingTheme'=> __( 'Deleting AVIF files for the theme folder...', 'veloimg' ),
     1041                                        'deleteFailed' => __( 'Could not delete AVIF.', 'veloimg' ),
     1042                                        'deleted'      => __( 'AVIF files were deleted.', 'veloimg' ),
     1043                                        'deletedHint'  => __( 'You can run generation again.', 'veloimg' ),
     1044                                        'deleteConfirm'=> __( 'Delete all AVIF files in the selected folder? Original files will not be touched.', 'veloimg' ),
     1045                                        'deleteThemeConfirm' => __( 'Delete all AVIF files for the selected theme folder? Original files will not be touched.', 'veloimg' ),
     1046                                        'deleteAllConfirm' => __( 'Delete ALL generated AVIF files across the Media Library and theme derivatives? Original files will not be touched.', 'veloimg' ),
     1047                                        'noFolderData' => __( 'No file data available.', 'veloimg' ),
     1048                                        'open'         => __( 'Open', 'veloimg' ),
     1049                                        'avif'         => __( 'AVIF', 'veloimg' ),
     1050                                        'file'         => __( 'File', 'veloimg' ),
     1051                                        'status'       => __( 'Status', 'veloimg' ),
     1052                                        'origSize'     => __( 'Original', 'veloimg' ),
     1053                                        'saving'       => __( 'Savings', 'veloimg' ),
     1054                                        'loadedCount'  => __( 'loaded', 'veloimg' ),
     1055                                        'loadingMore'  => __( 'Loading...', 'veloimg' ),
     1056                                        'scrollToLoadMore' => __( 'Scroll down to load more...', 'veloimg' ),
     1057                                        'allLoaded'    => __( 'All rows are loaded.', 'veloimg' ),
     1058                                ),
     1059                        )
     1060                );
     1061
     1062                include VELOWP_DIR . 'templates/admin-avif.php';
     1063                self::render_navigation_config( self::AVIF_SLUG );
     1064        }
     1065
     1066        public static function render() : void {
     1067                if ( ! current_user_can( self::CAPABILITY ) ) {
     1068                        wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) );
     1069                }
     1070
     1071                $active_panel = 'mass';
     1072                if ( isset( $_GET['settings-updated'] ) ) {
     1073                        $active_panel = 'setup';
     1074                }
     1075
     1076                $report   = Checker::check();
     1077                $settings = wp_parse_args( (array) get_option( 'velowp_settings', array() ), self::default_settings() );
     1078                $theme_asset_candidates = self::discover_theme_asset_folders();
     1079                $theme_asset_selected   = isset( $settings['theme_asset_folders'] ) && is_array( $settings['theme_asset_folders'] ) ? array_values( array_unique( array_map( 'strval', (array) $settings['theme_asset_folders'] ) ) ) : array();
     1080
     1081                $store  = DerivativesStore::default_store();
     1082                $mapper = new PathMapper( $store );
     1083                $repo   = new QueueRepository();
     1084
    10391085
    10401086                $scan = array(
    … …  
    11741220                $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION;
    11751221                wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver );
    1176                 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array(), $folder_runtime_ver, true );
     1222                wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true );
    11771223                wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true );
    1178                 wp_localize_script(
     1224                self::localize_admin_script(
    11791225                        'velowp-admin',
    11801226                        'velowpAdmin',
    … …  
    11821228                                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    11831229                                'nonce' => wp_create_nonce( self::MASS_NONCE_ACTION ),
     1230                                'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ),
    11841231                                'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'webp' ),
    11851232                                'initialGlobalSummary' => OptimizationSummary::read( 'webp' ),
    … …  
    11931240                                        'unableStats'  => __( 'Could not load folder information', 'veloimg' ),
    11941241                                        'chooseFolder' => __( 'Choose a folder.', 'veloimg' ),
    1195                                         'emptyFolder'  => __( 'No images were found in the Media Library for this folder.', 'veloimg' ),
     1242                                        'emptyFolder'  => __( 'No images were found in uploads for this folder.', 'veloimg' ),
    11961243                                        'nothingToDo'  => __( 'Everything in this folder is already optimized.', 'veloimg' ),
    11971244                                        'ready'        => __( 'Ready.', 'veloimg' ),
    … …  
    12351282                                        'chooseFolder'   => __( 'Choose a folder.', 'veloimg' ),
    12361283                                        'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ),
    1237                                         'emptyFolder'    => __( 'No images were found in the Media Library for this folder.', 'veloimg' ),
     1284                                        'emptyFolder'    => __( 'No images were found in uploads for this folder.', 'veloimg' ),
    12381285                                        'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ),
    12391286                                        'nothingToDo'    => __( 'Everything in this folder is already optimized.', 'veloimg' ),
    … …  
    12491296                                        'doneLabel'      => __( 'Generation completed', 'veloimg' ),
    12501297                                        'continueRun'    => __( 'Continue', 'veloimg' ),
    1251                                         'startAll'       => __( 'Start for the entire Media Library', 'veloimg' ),
     1298                                        'startAll'       => __( 'Start for all uploads', 'veloimg' ),
    12521299                                        'startFolder'    => __( 'Start generation', 'veloimg' ),
    12531300                                        'startFailed'    => __( 'Could not start', 'veloimg' ),
    12541301                                        'startTheme'     => __( 'Start for the theme folder', 'veloimg' ),
    12551302                                        'stepFailed'     => __( 'Step failed', 'veloimg' ),
    1256                                         'loadingAll'     => __( 'Loading Media Library information...', 'veloimg' ),
     1303                                        'loadingAll'     => __( 'Loading uploads information...', 'veloimg' ),
    12571304                                        'noDataYet'      => __( 'No data yet.', 'veloimg' ),
    12581305                                        'noFolderData'   => __( 'No file data available.', 'veloimg' ),
    … …  
    12941341                                'networkChanged'     => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ),
    12951342                                'loadingStats'       => __( 'Loading folder information...', 'veloimg' ),
     1343                                /* translators: %d: number of unsuccessful files eligible for retry. */
     1344                                'retryCount'         => __( 'Files eligible for retry in this selection: %d', 'veloimg' ),
     1345                                /* translators: %d: number of unsuccessful files selected for retry. */
     1346                                'retryStart'         => __( 'Retry processing %d files', 'veloimg' ),
     1347                                'retryRunning'       => __( 'Retry processing is running. Progress and Stop are in the generation block above.', 'veloimg' ),
     1348                                'retryUnavailable'   => __( 'Retry processing will be available after statistics are updated and generation is stopped or completed.', 'veloimg' ),
    12961349                                'loadingTheme'       => __( 'Loading theme folder information...', 'veloimg' ),
    12971350                                'unableStats'        => __( 'Could not load folder information', 'veloimg' ),
    12981351                                'chooseFolder'       => __( 'Choose a folder.', 'veloimg' ),
    12991352                                'chooseThemeFolder'  => __( 'Choose a theme folder.', 'veloimg' ),
    1300                                 'emptyFolder'        => __( 'No images were found in the Media Library for this folder.', 'veloimg' ),
     1353                                'emptyFolder'        => __( 'No images were found in uploads for this folder.', 'veloimg' ),
    13011354                                'emptyThemeFolder'   => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ),
    13021355                                'nothingToDo'        => __( 'Everything in this folder is already optimized.', 'veloimg' ),
    … …  
    13171370                                'doneLabel'          => __( 'Generation completed', 'veloimg' ),
    13181371                                'continueRun'        => __( 'Continue', 'veloimg' ),
    1319                                 'startAll'           => __( 'Start for the entire Media Library', 'veloimg' ),
     1372                                'startAll'           => __( 'Start for all uploads', 'veloimg' ),
    13201373                                'startFolder'        => __( 'Start generation', 'veloimg' ),
    13211374                                'startFailed'        => __( 'Could not start', 'veloimg' ),
    13221375                                'startTheme'         => __( 'Start for the theme folder', 'veloimg' ),
    13231376                                'stepFailed'         => __( 'Step failed', 'veloimg' ),
    1324                                 'loadingAll'         => __( 'Loading Media Library information...', 'veloimg' ),
     1377                                'loadingAll'         => __( 'Loading uploads information...', 'veloimg' ),
    13251378                                'noDataYet'          => __( 'No data yet.', 'veloimg' ),
    13261379                                'noFolderData'       => __( 'No file data available.', 'veloimg' ),
    … …  
    13631416                                'queuePendingOne'    => __( 'In queue', 'veloimg' ),
    13641417                                /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */
    1365                                 'outsideMonthsSummary' => __( 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ),
     1418                                'outsideMonthsSummary' => __( 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ),
    13661419                                'queueProcessing'    => __( 'Processing', 'veloimg' ),
    13671420                                'queueProcessingOne' => __( 'Processing', 'veloimg' ),
    … …  
    14411494                wp_enqueue_media();
    14421495
     1496                $admin_i18n_overrides['i18n'] = array_replace( self::$admin_page_configs['velowpAdmin']['i18n'], $admin_i18n_overrides['i18n'] );
     1497                self::$admin_page_configs['velowpAdmin'] = array_replace( self::$admin_page_configs['velowpAdmin'], $admin_i18n_overrides );
    14431498                include VELOWP_DIR . 'templates/admin-dashboard.php';
     1499                self::render_navigation_config( self::WEBP_SLUG );
    14441500                return;
    14451501        }
    14461502
     1503        /** Keep the native page and its AJAX fragment on the same configuration path. */
     1504        private static function localize_admin_script( string $handle, string $object, array $data ) : void {
     1505                self::$admin_page_configs[ $object ] = $data;
     1506                wp_localize_script( $handle, $object, $data );
     1507        }
     1508
     1509        private static function render_navigation_config( string $page ) : void {
     1510                $pages = array( self::SLUG, self::WEBP_SLUG, self::AVIF_SLUG, self::HELP_SLUG );
     1511                $labels = array(
     1512                        self::SLUG => __( 'VeloIMG', 'veloimg' ),
     1513                        self::WEBP_SLUG => __( 'WebP generation', 'veloimg' ),
     1514                        self::AVIF_SLUG => __( 'AVIF generation', 'veloimg' ),
     1515                        self::HELP_SLUG => __( 'Help / FAQ', 'veloimg' ),
     1516                );
     1517                foreach ( array( 'admin-navigation', 'folder-statistics-runtime', 'admin', 'admin-avif' ) as $asset ) {
     1518                        $file = 'assets/js/' . $asset . '.js';
     1519                        $version = is_file( VELOWP_DIR . $file ) ? (string) (int) filemtime( VELOWP_DIR . $file ) : VELOWP_VERSION;
     1520                        $deps = 'admin-navigation' === $asset ? array( 'jquery' ) : array( 'jquery', 'velowp-admin-navigation' );
     1521                        if ( in_array( $asset, array( 'admin', 'admin-avif' ), true ) ) { $deps[] = 'velowp-folder-statistics-runtime'; }
     1522                        wp_enqueue_script( 'velowp-' . $asset, VELOWP_URL . $file, $deps, $version, true );
     1523                }
     1524                // The media picker is available after any internal transition, not just a native WebP load.
     1525                wp_enqueue_media();
     1526                $packet = array(
     1527                        'page' => $page,
     1528                        'title' => $labels[ $page ] . ' ‹ VeloIMG — ' . get_bloginfo( 'name' ),
     1529                        'admin' => self::$admin_page_configs['velowpAdmin'] ?? array(),
     1530                        'avif' => self::$admin_page_configs['velowpAvifAdmin'] ?? array(),
     1531                        'navigation' => array(
     1532                                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     1533                                'pageUrl' => admin_url( 'admin.php' ),
     1534                                'pages' => $pages,
     1535                                'nonce' => wp_create_nonce( 'velowp_admin_navigation' ),
     1536                                'i18n' => array(
     1537                                        'loading' => __( 'Loading section...', 'veloimg' ),
     1538                                        'saving' => __( 'Sending request...', 'veloimg' ),
     1539                                        'failed' => __( 'Could not load the section. The current page is still available.', 'veloimg' ),
     1540                                        'saveFailed' => __( 'The server response could not be confirmed. Open the page to check the result before trying again.', 'veloimg' ),
     1541                                        'openPage' => __( 'Open page', 'veloimg' ),
     1542                                        'unsaved' => __( 'There are unsaved settings. Leave this section without saving?', 'veloimg' ),
     1543                                ),
     1544                        ),
     1545                );
     1546                // JSON is inert page data, never executable inline code.
     1547                echo '<script type="application/json" id="velowp-admin-page-config">' . wp_json_encode( $packet, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON_HEX_* encodes HTML metacharacters in this inert JSON data block.
     1548        }
     1549
     1550        public static function ajax_admin_page() : void {
     1551                if ( ! current_user_can( self::CAPABILITY ) || ! check_ajax_referer( 'velowp_admin_navigation', 'nonce', false ) ) {
     1552                        wp_send_json_error( array( 'message' => __( 'Security check failed.', 'veloimg' ) ), 403 );
     1553                }
     1554                $page = isset( $_POST['page'] ) ? sanitize_key( wp_unslash( $_POST['page'] ) ) : '';
     1555                $renderers = array( self::SLUG => 'render_home', self::WEBP_SLUG => 'render', self::AVIF_SLUG => 'render_avif', self::HELP_SLUG => 'render_help' );
     1556                if ( ! isset( $renderers[ $page ] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid request.', 'veloimg' ) ), 400 ); }
     1557                $request_post = $_POST;
     1558                $request_get = $_GET;
     1559                $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
     1560                // This endpoint only renders. Native trial/save POSTs never enter this path.
     1561                $_POST = array();
     1562                $_GET = array( 'page' => $page );
     1563                if ( ! empty( $request_post['settings_updated'] ) ) { $_GET['settings-updated'] = '1'; }
     1564                // wp_nonce_field()/Settings API must refer back to the real page, not admin-ajax.php.
     1565                $_SERVER['REQUEST_URI'] = (string) wp_parse_url( admin_url( 'admin.php' ), PHP_URL_PATH ) . '?' . http_build_query( $_GET );
     1566                ob_start();
     1567                try { self::{ $renderers[ $page ] }(); $html = ob_get_contents(); }
     1568                finally { ob_end_clean(); $_POST = $request_post; $_GET = $request_get; $_SERVER['REQUEST_URI'] = $request_uri; }
     1569                wp_send_json_success( array( 'html' => $html ) );
     1570        }
    14471571
    14481572        private static function find_quick_tick_item( array $tick, string $original_rel ) : array {
    … …  
    18321956                );
    18331957
    1834                 return array_slice( $items, 0, 10 );
     1958                return $items;
     1959        }
     1960
     1961        /** Read existing evidence only: never convert images while building the picker. */
     1962        private static function home_trial_candidates( array $items, PathMapper $mapper, string $format, array $quick, array $settings ) : array {
     1963                $paths = array_column( $items, 'rel' );
     1964                $is_avif = 'avif' === $format;
     1965                $records = $is_avif
     1966                        ? ( new AvifResultsRepository() )->map_by_source_paths( 'uploads', $paths )
     1967                        : ( new WebpResultsRepository() )->map_by_source_paths( 'uploads', 'uploads', $paths );
     1968                $queue = $is_avif ? array() : self::queue_map_for_originals( $paths );
     1969                $avif_processor = $is_avif ? new AvifProcessor( DerivativesStore::default_store(), $mapper, array(), $settings ) : null;
     1970                $ready = array();
     1971                $untested_jpeg = array();
     1972                $untested_png = array();
     1973                foreach ( $items as $item ) {
     1974                        $rel = (string) ( $item['rel'] ?? '' );
     1975                        $src_abs = $mapper->original_abs_from_original_rel( $rel );
     1976                        if ( ! is_file( $src_abs ) || is_link( $src_abs ) ) {
     1977                                continue;
     1978                        }
     1979                        $src_mtime = (int) filemtime( $src_abs );
     1980                        $src_bytes = (int) filesize( $src_abs );
     1981                        $row = (array) ( $records[ $rel ] ?? array() );
     1982                        $last_trial = $rel === (string) ( $quick['original_rel'] ?? '' ) && ! empty( $quick['verdict']['done'] );
     1983                        $last_passed = $last_trial && ! empty( $quick['verdict']['passed'] );
     1984                        $dst_abs = $is_avif ? $avif_processor->derivative_abs_from_original_rel( $rel ) : $mapper->derivative_abs_from_original_rel( $rel );
     1985                        $is_png = 'png' === strtolower( (string) pathinfo( $rel, PATHINFO_EXTENSION ) );
     1986                        $usable = is_file( $dst_abs ) && ! is_link( $dst_abs )
     1987                                && (int) filesize( $dst_abs ) > 0 && (int) filesize( $dst_abs ) <= $src_bytes
     1988                                && (int) filemtime( $dst_abs ) >= $src_mtime
     1989                                && ( $is_avif ? self::is_avif_file( $dst_abs ) : self::is_webp_file( $dst_abs ) );
     1990                        // PNG/AVIF needs previous successful validation, not just a container header.
     1991                        if ( $usable && ( ! $is_avif || ! $is_png || $last_passed || 'done' === (string) ( $row['status'] ?? '' ) ) ) {
     1992                                $ready[] = $item;
     1993                                if ( count( $ready ) >= 10 ) {
     1994                                        break;
     1995                                }
     1996                                continue;
     1997                        }
     1998                        $record_current = $is_avif
     1999                                ? (int) ( $row['src_bytes'] ?? 0 ) === $src_bytes
     2000                                : ( (int) ( $row['original_mtime'] ?? -1 ) === $src_mtime && (int) ( $row['src_bytes'] ?? -1 ) === $src_bytes );
     2001                        $queue_status = (string) ( $queue[ $rel ]['status'] ?? '' );
     2002                        $queue_failed = 'error' === $queue_status || ( 'skipped' === $queue_status && 'up_to_date' !== (string) ( $queue[ $rel ]['last_error'] ?? '' ) );
     2003                        if ( ( $record_current && in_array( (string) ( $row['status'] ?? '' ), array( 'error', 'skipped' ), true ) )
     2004                                || $queue_failed
     2005                                || ( $last_trial && ! $last_passed ) ) {
     2006                                continue;
     2007                        }
     2008                        if ( $is_png ) {
     2009                                $untested_png[] = $item;
     2010                        } else {
     2011                                $untested_jpeg[] = $item;
     2012                        }
     2013                }
     2014                return array_slice( array_merge( $ready, $untested_jpeg, $untested_png ), 0, 10 );
    18352015        }
    18362016
    … …  
    23072487                        ? __( 'Another run control request is in progress. Wait briefly and try again.', 'veloimg' )
    23082488                        : __( 'The run state could not be confirmed. Synchronize the page before retrying.', 'veloimg' );
     2489                if ( 'retry_quality_invalid' === $code ) { $message = __( 'Choose a retry quality between 50 and 100.', 'veloimg' ); }
     2490                if ( 'retry_engine_unavailable' === $code ) { $message = __( 'The selected WebP encoder is unavailable. Refresh the page and select an available encoder.', 'veloimg' ); }
    23092491                $result = array( 'ok' => false, 'code' => $code, 'message' => $message, 'retry_after' => 2 );
    23102492                try { $result['state'] = WebpRunState::state(); } catch ( WebpStateException $read_error ) { $result['state_unavailable'] = true; }
    … …  
    39244106                $all_original_bytes = 0;
    39254107                $last_activity_ts = 0;
     4108                $retry_paths = array();
    39264109                foreach ( $theme_files as $status ) {
    39274110                        $all_original_bytes += max( 0, (int) ( $status['src_bytes'] ?? 0 ) );
    … …  
    39314114                        $result_status = (string) ( $result['status'] ?? '' );
    39324115                        $is_unsuccessful = ! $is_ready && in_array( $result_status, array( 'skipped', 'error' ), true );
     4116                        if ( $is_unsuccessful && WebpRetry::candidate( trailingslashit( (string) $status['root_dir'] ) . (string) $status['rel'], (string) ( $result['reason'] ?? '' ) ) ) {
     4117                                $retry_paths[] = (string) $status['root_slug'] . '/' . (string) $status['rel'];
     4118                        }
    39334119
    39344120                        if ( $is_ready ) {
    … …  
    39864172                }
    39874173
     4174                $snapshot['retryable'] = count( $retry_paths );
    39884175                return self::$theme_preview_request_cache[ $cache_key ] = array(
     4176                        'retry_paths'      => $retry_paths,
    39894177                        'files'            => $files,
    39904178                        'reasons'          => $reasons,
    … …  
    41814369                        $summary    = array(
    41824370                                'total'          => $total,
     4371                                'retryable'      => (int) ( $theme_snapshot['retryable'] ?? 0 ),
    41834372                                'success'        => $done,
    41844373                                'failed'         => $failed,
    … …  
    42914480                $locked_until = ! empty( $lock['locked'] ) ? (int) ( $lock['until'] ?? 0 ) : 0;
    42924481                $now_ts = (int) current_time( 'timestamp' );
    4293                 $folder_label = ( '__all__' === $folder_rel ) ? __( 'Entire Media Library', 'veloimg' ) : $folder_rel;
     4482                $folder_label = ( '__all__' === $folder_rel ) ? __( 'All uploads', 'veloimg' ) : $folder_rel;
    42944483
    42954484                $mass_state = WebpRunState::state();
    … …  
    43004489                $summary = array(
    43014490                        'total'          => $total,
     4491                        'retryable'      => (int) ( $physical['retryable'] ?? 0 ),
    43024492                        'success'        => $done,
    43034493                        'failed'         => $failed,
    … …  
    55165706                                'queue' => $queue,
    55175707                                'skipped_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ),
    5518                                 'summary' => array( 'total' => $total, 'success' => $success, 'failed' => $failed, 'processed' => (int) ( $physical['processed'] ?? 0 ), 'remaining' => (int) ( $physical['remaining'] ?? 0 ), 'pending' => (int) ( $physical['pending'] ?? 0 ), 'processing' => (int) ( $physical['processing'] ?? 0 ), 'failed_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ), 'outside_months' => (int) ( $all_physical['outside_months'] ?? 0 ), 'outside_summary' => (array) ( $all_physical['outside_summary'] ?? array() ), 'jpg' => (int) ( $physical['jpg'] ?? 0 ), 'png' => (int) ( $physical['png'] ?? 0 ) ),
     5708                                'summary' => array( 'total' => $total, 'retryable' => (int) ( $physical['retryable'] ?? 0 ), 'success' => $success, 'failed' => $failed, 'processed' => (int) ( $physical['processed'] ?? 0 ), 'remaining' => (int) ( $physical['remaining'] ?? 0 ), 'pending' => (int) ( $physical['pending'] ?? 0 ), 'processing' => (int) ( $physical['processing'] ?? 0 ), 'failed_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ), 'outside_months' => (int) ( $all_physical['outside_months'] ?? 0 ), 'outside_summary' => (array) ( $all_physical['outside_summary'] ?? array() ), 'jpg' => (int) ( $physical['jpg'] ?? 0 ), 'png' => (int) ( $physical['png'] ?? 0 ) ),
    55195709                                'global_summary' => self::global_optimization_summary( $all_physical ),
    55205710                                'heartbeat' => $heartbeat,
    … …  
    55455735                $folder_rel = (string) $reservation['folder'];
    55465736                if ( 'theme' === $source ) {
    5547                         $items = (array) ( self::theme_folder_files_slice( $folder_rel, 0, 0 )['items'] ?? array() );
    5548                         $labels = array();
    5549                         $preview = array( 'total' => count( $items ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 );
    5550                         foreach ( $items as $item ) {
    5551                                 $labels[] = (string) $item['root_slug'] . '/' . (string) $item['rel'];
    5552                                 if ( ! empty( $item['ready'] ) ) {
    5553                                         $preview['done']++;
    5554                                         $preview['bytes_src_total'] += (int) $item['src_bytes'];
    5555                                         $preview['bytes_webp_total'] += (int) $item['dst_bytes'];
     5737                        if ( ! empty( $reservation['retry'] ) ) {
     5738                                $labels = (array) ( self::build_theme_folder_preview( $folder_rel )['retry_paths'] ?? array() );
     5739                                if ( empty( $labels ) ) { throw new WebpStateException( 'retry_no_files' ); }
     5740                                $preview = array( 'total' => count( $labels ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 );
     5741                        } else {
     5742                                $items = (array) ( self::theme_folder_files_slice( $folder_rel, 0, 0 )['items'] ?? array() );
     5743                                $labels = array();
     5744                                $preview = array( 'total' => count( $items ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 );
     5745                                foreach ( $items as $item ) {
     5746                                        $labels[] = (string) $item['root_slug'] . '/' . (string) $item['rel'];
     5747                                        if ( ! empty( $item['ready'] ) ) {
     5748                                                $preview['done']++;
     5749                                                $preview['bytes_src_total'] += (int) $item['src_bytes'];
     5750                                                $preview['bytes_webp_total'] += (int) $item['dst_bytes'];
     5751                                        }
    55565752                                }
    55575753                        }
    … …  
    55875783
    55885784                $manifest = new WebpManifestMassOrchestrator();
    5589                 $sealed = $manifest->build_sealed( $folder_rel );
     5785                $retry_paths = null;
     5786                if ( ! empty( $reservation['retry'] ) ) {
     5787                        UploadsPhysicalScopeSummary::invalidate_request_cache();
     5788                        $retry_paths = ( new UploadsPhysicalScopeSummary() )->retry_paths( $folder_rel );
     5789                        if ( empty( $retry_paths ) ) { throw new WebpStateException( 'retry_no_files' ); }
     5790                }
     5791                $sealed = $manifest->build_sealed( $folder_rel, $retry_paths );
    55905792                if ( empty( $sealed['ok'] ) ) {
    55915793                        throw new WebpStateException( 'manifest_build_failed' );
    … …  
    55985800                        throw new WebpStateException( 'manifest_build_invalid' );
    55995801                }
    5600                 try { $statistics = WebpRunStatistics::initialize( $manifest_id ); }
     5802                try { $statistics = WebpRunStatistics::initialize( $manifest_id, ! empty( $reservation['retry'] ) ); }
    56015803                catch ( \RuntimeException $error ) { throw new WebpStateException( esc_html( $error->getMessage() ) ); }
    56025804
    … …  
    56365838                catch ( WebpStateException $error ) {
    56375839                        $state = WebpRunState::fail( $reservation, $error->reason(), static function () : void { self::assert_mass_owner(); }, true );
    5638                         return array( 'ok' => false, 'code' => $error->reason(), 'state' => $state, 'message' => __( 'Physical uploads manifest could not be sealed. No generation was started.', 'veloimg' ) );
     5840                        return array( 'ok' => false, 'code' => $error->reason(), 'state' => $state, 'message' => 'retry_no_files' === $error->reason() ? __( 'No unsuccessful WebP files eligible for retry remain in this selection. Refresh the statistics.', 'veloimg' ) : __( 'Physical uploads manifest could not be sealed. No generation was started.', 'veloimg' ) );
    56395841                }
    56405842                // A Stop during the builder keeps stopping; publication cannot reopen the activation.
    … …  
    56665868                if ( empty( $san['ok'] ) ) { throw new WebpStateException( 'mass_folder_invalid' ); }
    56675869                if ( 'uploads' === $source && ! Installer::exact_identity_cutover_ready() ) { throw new WebpStateException( 'exact_identity_not_ready' ); }
     5870                $retry = array();
     5871                if ( '' !== self::request_text( 'retry_quality' ) ) {
     5872                        $report = Checker::check();
     5873                        try { $retry = WebpRetry::options( self::request_text( 'retry_quality' ), self::request_key( 'retry_engine', 'auto' ), (array) ( $report['caps'] ?? array() ) ); }
     5874                        catch ( \InvalidArgumentException $error ) { throw new WebpStateException( esc_html( $error->getMessage() ) ); }
     5875                }
    56685876                return array(
    56695877                        'source' => $source,
    56705878                        'folder' => (string) $san['folder'],
    56715879                        'mode' => $mode,
     5880                        'retry' => $retry,
    56725881                        // These values only preserve the already rendered snapshot while the
    56735882                        // new immutable manifest is sealed. They are not worker counters.
    … …  
    58006009
    58016010                                try {
    5802                                         $result = $resolver->generate_theme_asset_result( (string) ( $item['original_url'] ?? '' ) );
     6011                                        $result = $resolver->generate_theme_asset_result( (string) ( $item['original_url'] ?? '' ), (array) ( $state['retry'] ?? array() ) );
    58036012                                        $generation = (array) ( $result['generation_result'] ?? array() );
    58046013                                        $post_status = self::theme_file_status(
  • veloimg/trunk/src/Conversion/UploadsPhysicalScopeSummary.php

    r3728637 r3739629  
    3939                $scope = $this->normalize_scope( $scope );
    4040                return '__all__' === $scope ? (array) $all['all'] : (array) ( $all['groups'][ $scope ] ?? $this->empty_summary( $scope ) );
     41        }
     42
     43        /** Selected terminal failures from the same read-only classification as the UI. */
     44        public function retry_paths( string $scope ) : array {
     45                $all = $this->all();
     46                return (array) ( $all['retry_paths'][ $this->normalize_scope( $scope ) ] ?? array() );
    4147        }
    4248
    … …  
    5157                $outside = $this->empty_summary( '' );
    5258                $groups = array();
     59                $retry_paths = array( '__all__' => array() );
    5360                $queue = $this->queue_states();
    5461                $activity = $this->activity_by_original( $queue['activity'], $this->files_activity() );
    … …  
    7380                        if ( null === $file ) { continue; }
    7481                        $group = $this->group_for( $original_rel );
     82                        if ( ! empty( $file['retryable'] ) ) {
     83                                $retry_paths['__all__'][] = $original_rel;
     84                                if ( '' !== $group ) { $retry_paths[ $group ][] = $original_rel; }
     85                        }
    7586                        if ( '' !== $group && ! isset( $groups[ $group ] ) ) {
    7687                                $groups[ $group ] = $this->empty_summary( $group );
    … …  
    92103                unset( $summary );
    93104                ksort( $groups, SORT_STRING );
    94                 self::$request_cache[ $key ] = array( 'all' => $all, 'groups' => $groups, 'queue_rows' => $queue['rows'], 'source_file_count' => $all['total'] );
     105                self::$request_cache[ $key ] = array( 'all' => $all, 'groups' => $groups, 'queue_rows' => $queue['rows'], 'source_file_count' => $all['total'], 'retry_paths' => $retry_paths );
    95106                return self::$request_cache[ $key ];
    96107        }
    … …  
    113124                if ( $failed ) { $reason = FailureReasons::normalize( FailureReasons::WEBP, (string) ( $errors[ $original_rel ] ?? 'not_saved' ) ); }
    114125                return array( 'type' => 'png' === $ext ? 'png' : 'jpg', 'ready' => $ready, 'failed' => $failed,
     126                        'retryable' => $failed && WebpRetry::candidate( $original_abs, $reason ),
    115127                        'pending' => ! $ready && 'pending' === $status, 'processing' => ! $ready && 'processing' === $status,
    116128                        'reason' => $reason, 'all_original_bytes' => $src_bytes, 'src_bytes' => $ready ? $src_bytes : 0,
    … …  
    121133        private function add_file( array &$summary, array $file ) : void {
    122134                $summary['total']++; $summary[ $file['type'] ]++;
    123                 foreach ( array( 'ready', 'failed', 'pending', 'processing' ) as $field ) { $summary[ $field ] += (int) $file[ $field ]; }
     135                foreach ( array( 'ready', 'failed', 'pending', 'processing', 'retryable' ) as $field ) { $summary[ $field ] = (int) ( $summary[ $field ] ?? 0 ) + (int) $file[ $field ]; }
    124136                $summary['bytes_src_total'] += $file['src_bytes']; $summary['bytes_webp_total'] += $file['webp_bytes'];
    125137                $summary['all_original_bytes'] += $file['all_original_bytes'];
    … …  
    175187        /** @return array<string,mixed> */
    176188        private function empty_summary( string $scope ) : array {
    177                 return array( 'scope' => $scope, 'total' => 0, 'ready' => 0, 'failed' => 0, 'pending' => 0, 'processing' => 0, 'processed' => 0, 'remaining' => 0, 'outside_months' => 0, 'reason_counts' => array(), 'jpg' => 0, 'png' => 0, 'all_original_bytes' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0, 'last_activity' => '' );
     189                return array( 'scope' => $scope, 'total' => 0, 'ready' => 0, 'failed' => 0, 'retryable' => 0, 'pending' => 0, 'processing' => 0, 'processed' => 0, 'remaining' => 0, 'outside_months' => 0, 'reason_counts' => array(), 'jpg' => 0, 'png' => 0, 'all_original_bytes' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0, 'last_activity' => '' );
    178190        }
    179191
  • veloimg/trunk/src/Conversion/WebpManifestMassOrchestrator.php

    r3678590 r3739629  
    2929         * @return array<string,mixed>
    3030         */
    31         public function build_sealed( string $folder_rel ) : array {
     31        public function build_sealed( string $folder_rel, ?array $selected_paths = null ) : array {
    3232                $scope = $this->scope( $folder_rel );
    3333                if ( '' === $scope ) {
    … …  
    3535                }
    3636
    37                 $paths = $this->inventory->files_for_folder( $scope );
     37                // A retry supplies server-classified failures, never paths from the browser.
     38                $paths = null === $selected_paths ? $this->inventory->files_for_folder( $scope ) : $selected_paths;
    3839                $begin = $this->manifests->begin_snapshot( 'uploads', $scope );
    3940                if ( empty( $begin['ok'] ) ) {
  • veloimg/trunk/src/Database/WebpRunStatistics.php

    r3715784 r3739629  
    6161
    6262        /** Initial reconciliation is once at Start, never in a polling/result loop. */
    63         public static function initialize( string $id ) : array {
     63        public static function initialize( string $id, bool $retry = false ) : array {
    6464                global $wpdb;
    6565                $manifests = new RunManifestRepository();
    … …  
    8484                        foreach ( $items as $item ) {
    8585                                $path = (string) $item['original_path'];
    86                                 if ( ! self::observe( $path, '', '', (array) ( $records[ $path ] ?? array() ), (array) ( $queues[ DerivativesStore::exact_path_key( $path ) ] ?? array() ) ) ) { throw new \RuntimeException( 'statistics_reconciliation_failed' ); }
     86                                // An explicit retry begins a new attempt for this sealed subset, not
     87                                // a run already at 100% because its previous failures were terminal.
     88                                if ( ! self::observe( $path, $retry ? 'pending' : '', '', (array) ( $records[ $path ] ?? array() ), (array) ( $queues[ DerivativesStore::exact_path_key( $path ) ] ?? array() ) ) ) { throw new \RuntimeException( 'statistics_reconciliation_failed' ); }
    8789                                $cursor = (int) $item['ordinal'];
    8890                        }
  • veloimg/trunk/src/Delivery/FormatStrategy.php

    r3715784 r3739629  
    88        /** @param array<string,mixed> $settings */
    99        public static function order_for( array $settings ) : array {
    10                 $strategy = sanitize_key( (string) ( $settings['image_strategy'] ?? 'webp_only' ) );
     10                $strategy = sanitize_key( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) );
    1111                $legacy = array(
    1212                        'max_savings' => 'avif_webp',
  • veloimg/trunk/src/Delivery/Resolver.php

    r3728637 r3739629  
    99use VeloWP\Core\FailureReasons;
    1010use VeloWP\Core\FileSystem;
     11use VeloWP\Conversion\WebpRetry;
    1112use VeloWP\Concurrency\AtomicLease;
    1213use VeloWP\Concurrency\LeaseLostException;
    … …  
    342343         * @return array<string,mixed>
    343344         */
    344         public function generate_theme_asset_result( string $url ) : array {
     345        public function generate_theme_asset_result( string $url, array $retry = array() ) : array {
    345346                if ( ! $this->theme_assets_mode_enabled() ) {
    346347                        return array( 'ok' => false );
    347348                }
    348                 return $this->resolve_theme_asset( $url, true, true );
     349                return $this->resolve_theme_asset( $url, true, true, $retry );
    349350        }
    350351
    … …  
    570571         * @return array<string,mixed>
    571572         */
    572         private function resolve_theme_asset( string $url, bool $allow_generation = true, bool $include_generation_result = false ) : array {
     573        private function resolve_theme_asset( string $url, bool $allow_generation = true, bool $include_generation_result = false, array $retry = array() ) : array {
    573574                $url      = self::strip_query( $url );
    574575                $url_path = (string) wp_parse_url( $url, PHP_URL_PATH );
    … …  
    624625                                                        AtomicLease::assert_owned( (string) $lease['token'] );
    625626                                                        if ( ! $this->is_theme_derivative_usable( $original_abs, $webp_abs ) ) {
    626                                                                 $generation_result = $this->ensure_theme_derivative_result( $original_abs, $webp_abs, $webp_rel, $webp_url );
     627                                                                $generation_result = $this->ensure_theme_derivative_result( $original_abs, $webp_abs, $webp_rel, $webp_url, $retry );
    627628                                                                AtomicLease::assert_owned( (string) $lease['token'] );
    628629                                                                $generated = ! empty( $generation_result['ok'] ) && 'done' === (string) ( $generation_result['status'] ?? '' );
    … …  
    674675         * @return array<string,mixed>
    675676         */
    676         private function ensure_theme_derivative_result( string $original_abs, string $webp_abs, string $webp_rel, string $webp_url ) : array {
     677        private function ensure_theme_derivative_result( string $original_abs, string $webp_abs, string $webp_rel, string $webp_url, array $retry = array() ) : array {
    677678                $ext = strtolower( (string) pathinfo( $original_abs, PATHINFO_EXTENSION ) );
    678679                if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'png' ), true ) ) {
    … …  
    697698
    698699                $max_bytes = $src_bytes > 0 ? $src_bytes : 0;
    699                 $settings    = (array) get_option( 'velowp_settings', array() );
     700                $settings    = WebpRetry::settings( (array) get_option( 'velowp_settings', array() ), $retry );
    700701                $quality     = $this->quality( $settings );
    701702                $png_mode    = $this->png_mode( $settings );
    702703                $webp_mode   = $this->webp_mode( $settings );
    703704                $png_quality = $this->png_quality( $settings );
    704                 $caps        = $this->caps();
     705                $caps        = WebpRetry::caps( $this->caps(), $retry );
    705706                if ( ( empty( $caps['imagick_webp'] ) || ! class_exists( '\Imagick' ) ) && ( empty( $caps['gd_webp'] ) || ! function_exists( 'imagewebp' ) ) ) {
    706707                        return $this->theme_generation_error( 'engine_missing', $src_bytes, 0, $webp_rel );
  • veloimg/trunk/src/Lifecycle/Activate.php

    r3728637 r3739629  
    2222                // Ensure default settings exist.
    2323                $defaults = array(
    24                         'image_strategy'          => 'webp_only',
     24                        'image_strategy'          => 'avif_webp',
    2525                        'delivery_method'         => 'off',
    2626                        'bypass_param'            => 'no_veloimg',
  • veloimg/trunk/src/Queue/Worker.php

    r3715784 r3739629  
    66
    77use VeloWP\Converter\Processor;
     8use VeloWP\Conversion\WebpRetry;
    89use VeloWP\Concurrency\LeaseLostException;
    910use VeloWP\Core\Checker;
    … …  
    181182                        $files_repo = new FilesRepository();
    182183                        $proc     = new Processor( $store, $mapper, $files_repo, (array) ( $report['caps'] ?? array() ), $settings, array( 'assert_ownership' => static function () use ( $token ) : void { Lock::assert_owned( $token ); } ) );
     184                        $retry_proc = null;
    183185
    184186                        foreach ( $pending as $task ) {
    … …  
    243245                                }
    244246
    245                                 $res = $proc->generate_from_rel( $orig, false );
     247                                // Generic ticks may also process automatic uploads. Only rows owned by
     248                                // this exact retry run receive its immutable settings, including after Resume.
     249                                $task_proc = $proc;
     250                                if ( '' !== $task_run_id && $task_run_id === (string) ( $mass_activation['run_id'] ?? '' ) && ! empty( $mass_activation['retry'] ) ) {
     251                                        if ( null === $retry_proc ) {
     252                                                $retry = (array) $mass_activation['retry'];
     253                                                $retry_proc = new Processor( $store, $mapper, $files_repo, WebpRetry::caps( (array) ( $report['caps'] ?? array() ), $retry ), WebpRetry::settings( $settings, $retry ), array( 'assert_ownership' => static function () use ( $token ) : void { Lock::assert_owned( $token ); } ) );
     254                                        }
     255                                        $task_proc = $retry_proc;
     256                                }
     257                                $res = $task_proc->generate_from_rel( $orig, false );
    246258                                Lock::assert_owned( $token );
    247259                                if ( empty( $res['ok'] ) ) {
  • veloimg/trunk/templates/admin-avif.php

    r3728637 r3739629  
    1212 * - $avif_jpeg_quality (int)
    1313 * - $avif_png_quality (int)
    14  * - $avif_candidates (array)
    15  * - $avif_quick (array)
    1614 * - $avif_folder_candidates (array)
    1715 * - $theme_asset_candidates (array)
    … …  
    2523}
    2624
    27 $velowp_avif_prefill = '';
    28 if ( empty( $avif_quick['submitted'] ) && empty( $avif_quick['input'] ) && ! empty( $avif_candidates[0]['url'] ) ) {
    29         $velowp_avif_prefill = (string) $avif_candidates[0]['url'];
    30 }
    31 $velowp_avif_quick_input_prefill = (string) ( $avif_quick['input'] ?? $velowp_avif_prefill );
    32 $velowp_avif_generate_attrs = array(
    33         'id' => 'velowp-avif-quick-generate',
    34         'data-velowp-avif-ready' => $avif_ready ? '1' : '0',
    35 );
    36 if ( '' === trim( $velowp_avif_quick_input_prefill ) || ! $avif_ready ) {
    37         $velowp_avif_generate_attrs['disabled'] = 'disabled';
    38 }
    39 
    4025$velowp_avif_mass_state  = isset( $avif_mass_status['state'] ) && is_array( $avif_mass_status['state'] ) ? (array) $avif_mass_status['state'] : array();
    4126$velowp_avif_mass_folder = (string) ( $velowp_avif_mass_state['folder'] ?? '' );
    … …  
    4328$velowp_avif_mass_is_theme = 'theme' === $velowp_avif_mass_source;
    4429$velowp_avif_panel       = 'mass';
    45 if ( ! empty( $avif_quick['submitted'] ) ) {
    46         $velowp_avif_panel = 'quick';
    47 } elseif ( empty( $avif_mass_status['active'] ) && isset( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Settings API redirect flag; only selects the visible panel.
     30if ( empty( $avif_mass_status['active'] ) && isset( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Settings API redirect flag; only selects the visible panel.
    4831        $velowp_avif_panel = 'setup';
    4932}
    … …  
    7457                </div>
    7558
    76                 <div class="velowp-topbar__right">
    77                         <button type="button" class="velowp-topbtn" data-panel="quick"><?php echo esc_html__( 'Quick check', 'veloimg' ); ?></button>
    78                 </div>
    7959        </div>
    8060
    … …  
    11191                                                                        <output class="velowp-range__out" id="velowp-avif-jpeg-quality-output"><?php echo esc_html( (string) $avif_jpeg_quality ); ?></output>
    11292                                                                </div>
     93                                                                <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div>
    11394                                                                <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality AVIF. Lower values reduce file size.', 'veloimg' ); ?></p>
    11495                                                        </td>
    … …  
    130111                                                                        <output class="velowp-range__out" id="velowp-avif-png-quality-output"><?php echo esc_html( (string) $avif_png_quality ); ?></output>
    131112                                                                </div>
     113                                                                <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div>
    132114                                                                <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality AVIF. Lower values reduce file size.', 'veloimg' ); ?></p>
    133115                                                                <p class="description"><?php echo esc_html__( 'These settings will be used by future AVIF generation. They do not start conversion by themselves.', 'veloimg' ); ?></p>
    … …  
    217199        </section>
    218200
    219         <section class="velowp-panel" data-panel="quick">
    220                 <form method="post" class="velowp-card velowp-quick-card" action="#quick">
    221                         <?php wp_nonce_field( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ); ?>
    222 
    223                         <input type="hidden" name="velowp_avif_quick_input" id="velowp-avif-quick-input" value="<?php echo esc_attr( $velowp_avif_quick_input_prefill ); ?>" />
    224 
    225                         <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Test AVIF generation on one file', 'veloimg' ); ?></h3>
    226                         <p class="description"><?php echo esc_html__( 'Pick one image and run a safe AVIF test conversion. This helps confirm that AVIF generation works before starting bulk processing.', 'veloimg' ); ?></p>
    227 
    228                         <div class="velowp-quick-inline velowp-quick-inline--top">
    229                                 <?php if ( ! empty( $avif_candidates ) ) : ?>
    230                                         <div class="velowp-quick-pick">
    231                                                 <select id="velowp-avif-candidate-select" class="velowp-select velowp-select--wide">
    232                                                         <option value=""><?php echo esc_html__( 'Select', 'veloimg' ); ?></option>
    233                                                         <?php foreach ( (array) $avif_candidates as $velowp_candidate ) : ?>
    234                                                                 <?php
    235                                                                         $velowp_candidate_url  = (string) ( $velowp_candidate['url'] ?? '' );
    236                                                                         $velowp_candidate_size = (int) ( $velowp_candidate['size'] ?? 0 );
    237                                                                         $velowp_width          = (int) ( $velowp_candidate['width'] ?? 0 );
    238                                                                         $velowp_height         = (int) ( $velowp_candidate['height'] ?? 0 );
    239                                                                         $velowp_candidate_label = '';
    240                                                                         if ( ! empty( $velowp_candidate['title'] ) ) {
    241                                                                                 $velowp_candidate_label .= (string) $velowp_candidate['title'] . ' - ';
    242                                                                         }
    243                                                                         if ( $velowp_width > 0 && $velowp_height > 0 ) {
    244                                                                                 $velowp_candidate_label .= $velowp_width . 'x' . $velowp_height;
    245                                                                         }
    246                                                                         if ( $velowp_candidate_size > 0 ) {
    247                                                                                 $velowp_candidate_label .= ' - ' . size_format( $velowp_candidate_size, 2 );
    248                                                                         }
    249                                                                         if ( '' === $velowp_candidate_label ) {
    250                                                                                 $velowp_candidate_label = $velowp_candidate_url;
    251                                                                         }
    252                                                                 ?>
    253                                                                 <option value="<?php echo esc_attr( $velowp_candidate_url ); ?>" <?php selected( $velowp_avif_quick_input_prefill, $velowp_candidate_url ); ?>><?php echo esc_html( $velowp_candidate_label ); ?></option>
    254                                                         <?php endforeach; ?>
    255                                                 </select>
    256                                         </div>
    257                                 <?php else : ?>
    258                                         <div class="velowp-quick-pick velowp-quick-pick--fallback">
    259                                                 <input
    260                                                         type="text"
    261                                                         name="velowp_avif_quick_input"
    262                                                         id="velowp-avif-quick-input-fallback"
    263                                                         class="regular-text velowp-quick-input"
    264                                                         value="<?php echo esc_attr( $velowp_avif_quick_input_prefill ); ?>"
    265                                                         placeholder="https://example.com/wp-content/uploads/2025/01/photo.jpg"
    266                                                 />
    267                                         </div>
    268                                 <?php endif; ?>
    269 
    270                                 <div class="velowp-actions velowp-actions--launch velowp-actions--inline">
    271                                         <button type="submit" class="button button-primary velowp-btn-glass velowp-btn-glass--primary"
    272                                                 <?php foreach ( $velowp_avif_generate_attrs as $velowp_attr_name => $velowp_attr_value ) : ?>
    273                                                         <?php if ( 'disabled' === (string) $velowp_attr_name ) : ?>
    274                                                                 disabled
    275                                                         <?php else : ?>
    276                                                                 <?php echo esc_attr( (string) $velowp_attr_name ); ?>="<?php echo esc_attr( (string) $velowp_attr_value ); ?>"
    277                                                         <?php endif; ?>
    278                                                 <?php endforeach; ?>
    279                                                 name="velowp_avif_quick_action" value="convert_now">
    280                                                 <?php echo esc_html__( 'Generate AVIF', 'veloimg' ); ?>
    281                                         </button>
    282                                 </div>
    283                         </div>
    284 
    285                         <?php if ( ! $avif_ready ) : ?>
    286                                 <p class="description"><?php echo esc_html__( 'AVIF generation is disabled until the server has an AVIF encoder and a writable derivatives store.', 'veloimg' ); ?></p>
    287                         <?php endif; ?>
    288 
    289                         <?php if ( ! empty( $avif_quick['submitted'] ) ) : ?>
    290                                 <hr class="velowp-hr velowp-hr--quick" />
    291                                 <h3 class="velowp-h3 velowp-h3--quick-result"><?php echo esc_html__( 'Result', 'veloimg' ); ?></h3>
    292 
    293                                 <?php if ( ! empty( $avif_quick['verdict']['done'] ) && empty( $avif_quick['verdict']['passed'] ) ) : ?>
    294                                         <div class="notice notice-warning inline velowp-quick-notice">
    295                                                 <p><strong><?php echo esc_html__( 'AVIF test did not create a usable file.', 'veloimg' ); ?></strong></p>
    296                                                 <?php if ( ! empty( $avif_quick['verdict']['note'] ) ) : ?>
    297                                                         <p class="description velowp-result-note"><?php echo esc_html( (string) $avif_quick['verdict']['note'] ); ?></p>
    298                                                 <?php endif; ?>
    299                                         </div>
    300                                 <?php elseif ( ! empty( $avif_quick['verdict']['passed'] ) ) : ?>
    301                                         <div class="notice notice-success inline velowp-quick-notice">
    302                                                 <p><?php echo esc_html( (string) ( $avif_quick['verdict']['summary'] ?? __( 'AVIF was created and passed validation.', 'veloimg' ) ) ); ?></p>
    303                                         </div>
    304                                 <?php endif; ?>
    305 
    306                                 <?php if ( ! empty( $avif_quick['original_rel'] ) ) : ?>
    307                                         <?php
    308                                                 $velowp_avif_url           = (string) ( $avif_quick['d_expected']['derivative_url'] ?? '' );
    309                                                 $velowp_avif_exists        = ! empty( $avif_quick['d_expected']['exists'] ) && '' !== $velowp_avif_url;
    310                                                 $velowp_original_bytes     = (int) ( $avif_quick['result']['original_bytes'] ?? 0 );
    311                                                 $velowp_avif_bytes         = (int) ( $avif_quick['result']['derivative_bytes'] ?? 0 );
    312                                                 $velowp_avif_saved_bytes   = 0;
    313                                                 $velowp_avif_saved_percent = 0;
    314                                                 $velowp_avif_is_valid      = ! empty( $avif_quick['verdict']['passed'] ) && $velowp_avif_exists && $velowp_avif_bytes > 0;
    315                                                 if ( $velowp_avif_is_valid && $velowp_original_bytes > 0 && $velowp_avif_bytes < $velowp_original_bytes ) {
    316                                                         $velowp_avif_saved_bytes   = (int) ( $velowp_original_bytes - $velowp_avif_bytes );
    317                                                         $velowp_avif_saved_percent = (int) round( ( $velowp_avif_saved_bytes / $velowp_original_bytes ) * 100 );
    318                                                 }
    319                                                 $velowp_avif_status = $velowp_avif_is_valid ? __( 'Exists and valid', 'veloimg' ) : ( $velowp_avif_exists ? __( 'Exists, but is not usable', 'veloimg' ) : __( 'Not created', 'veloimg' ) );
    320                                         ?>
    321                                         <div class="velowp-quick-result-grid">
    322                                                 <div class="velowp-quick-result-row">
    323                                                         <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original (relative path)', 'veloimg' ); ?></div>
    324                                                         <div class="velowp-quick-result-value"><code><?php echo esc_html( (string) $avif_quick['original_rel'] ); ?></code></div>
    325                                                 </div>
    326                                                 <div class="velowp-quick-result-row">
    327                                                         <div class="velowp-quick-result-label"><?php echo esc_html( $velowp_avif_exists ? __( 'AVIF URL', 'veloimg' ) : __( 'Expected AVIF path', 'veloimg' ) ); ?></div>
    328                                                         <div class="velowp-quick-result-value">
    329                                                                 <?php if ( '' !== $velowp_avif_url ) : ?>
    330                                                                         <div class="velowp-quick-result-url">
    331                                                                                 <code><?php echo esc_html( $velowp_avif_url ); ?></code>
    332                                                                                 <?php if ( $velowp_avif_exists ) : ?>
    333                                                                                         <span class="velowp-link-actions"><a class="button button-small velowp-btn-mini-open" href="<?php echo esc_url( $velowp_avif_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__( 'Open AVIF', 'veloimg' ); ?></a></span>
    334                                                                                 <?php endif; ?>
    335                                                                         </div>
    336                                                                         <?php if ( ! $velowp_avif_exists ) : ?>
    337                                                                                 <p class="description velowp-result-note"><?php echo esc_html__( 'The file does not exist yet, so it cannot be opened.', 'veloimg' ); ?></p>
    338                                                                         <?php endif; ?>
    339                                                                 <?php else : ?>
    340                                                                         <code>-</code>
    341                                                                 <?php endif; ?>
    342                                                         </div>
    343                                                 </div>
    344                                                 <div class="velowp-quick-result-row">
    345                                                         <div class="velowp-quick-result-label"><?php echo esc_html__( 'File status', 'veloimg' ); ?></div>
    346                                                         <div class="velowp-quick-result-value"><?php echo esc_html( $velowp_avif_status ); ?></div>
    347                                                 </div>
    348                                                 <?php if ( $velowp_original_bytes > 0 ) : ?>
    349                                                         <div class="velowp-quick-result-row">
    350                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original size', 'veloimg' ); ?></div>
    351                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_original_bytes, 2 ) ); ?></div>
    352                                                         </div>
    353                                                 <?php endif; ?>
    354                                                 <?php if ( $velowp_avif_bytes > 0 ) : ?>
    355                                                         <div class="velowp-quick-result-row">
    356                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'AVIF size', 'veloimg' ); ?></div>
    357                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_avif_bytes, 2 ) ); ?></div>
    358                                                         </div>
    359                                                 <?php endif; ?>
    360                                                 <?php if ( $velowp_avif_saved_bytes > 0 ) : ?>
    361                                                         <div class="velowp-quick-result-row">
    362                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Savings', 'veloimg' ); ?></div>
    363                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_avif_saved_bytes, 2 ) . ' (' . $velowp_avif_saved_percent . '%)' ); ?></div>
    364                                                         </div>
    365                                                 <?php endif; ?>
    366                                                 <?php if ( ! empty( $avif_quick['verdict']['reasons'] ) && is_array( $avif_quick['verdict']['reasons'] ) ) : ?>
    367                                                         <div class="velowp-quick-result-row">
    368                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Reason / details', 'veloimg' ); ?></div>
    369                                                                 <div class="velowp-quick-result-value">
    370                                                                         <ul class="velowp-list">
    371                                                                                 <?php foreach ( (array) $avif_quick['verdict']['reasons'] as $velowp_reason ) : ?>
    372                                                                                         <li><?php echo esc_html( (string) $velowp_reason ); ?></li>
    373                                                                                 <?php endforeach; ?>
    374                                                                         </ul>
    375                                                                 </div>
    376                                                         </div>
    377                                                 <?php endif; ?>
    378                                         </div>
    379                                 <?php endif; ?>
    380                         <?php endif; ?>
    381                 </form>
    382         </section>
    383201
    384202        <section class="velowp-panel" data-panel="mass">
    … …  
    389207                                                <div>
    390208                                                        <h2 class="velowp-h2 velowp-h2--tight"><?php echo esc_html__( 'AVIF generation', 'veloimg' ); ?></h2>
    391                                                         <p class="description"><?php echo esc_html__( 'Choose the entire Media Library, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created.', 'veloimg' ); ?></p>
     209                                                        <p class="description"><?php echo esc_html__( 'Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created.', 'veloimg' ); ?></p>
    392210                                                </div>
    393211                                        </div>
    … …  
    396214                                                <label class="velowp-seg">
    397215                                                        <input type="radio" name="velowp_avif_mass_mode" id="velowp-avif-mass-mode-all" value="all" <?php checked( ! $velowp_avif_mass_is_theme && ( '' === $velowp_avif_mass_folder || '__all__' === $velowp_avif_mass_folder ) ); ?> />
    398                                                         <span><?php echo esc_html__( 'Entire Media Library', 'veloimg' ); ?></span>
     216                                                        <span><?php echo esc_html__( 'All uploads', 'veloimg' ); ?></span>
    399217                                                </label>
    400218                                                <label class="velowp-seg">
    … …  
    484302                                        </div>
    485303                                        <p class="description" id="velowp-avif-mass-progress-text"><?php echo esc_html__( 'No AVIF generation run yet.', 'veloimg' ); ?></p>
     304                                        <?php $velowp_generation_format = 'avif'; require VELOWP_DIR . 'templates/generation-concurrency.php'; ?>
    486305
    487306                                        <div class="velowp-mass-actions velowp-mass-actions--row">
    … …  
    529348                                        <p id="velowp-avif-outside-months-summary" class="velowp-muted velowp-hidden"></p>
    530349
     350                                        <div class="velowp-table-scroll" tabindex="0" role="region" aria-label="<?php echo esc_attr__( 'Uploads folder statistics', 'veloimg' ); ?>">
    531351                                        <table class="widefat striped velowp-table-tight velowp-folders-table" id="velowp-avif-folders-overview">
    532352                                                <thead>
    … …  
    545365                                                </tbody>
    546366                                        </table>
     367                                        </div>
    547368                                        <p class="velowp-folder-more-wrap"><button type="button" class="button" id="velowp-avif-folders-more" hidden><?php echo esc_html__( 'Show more', 'veloimg' ); ?></button></p>
    548369                                </div>
  • veloimg/trunk/templates/admin-dashboard.php

    r3728637 r3739629  
    99 * - $lock_status (array)
    1010 * - $next_tick (int)
    11  * - $quick (array)
    12  * - $candidates (array)
    1311 * - $scan (array)
    1412 * - $active_panel (string)
    … …  
    2523
    2624$velowp_panel = is_string( $active_panel ?? null ) && '' !== (string) $active_panel ? (string) $active_panel : 'mass';
    27 
    28 // Prefill: pick the largest recent image.
    29 $velowp_prefill = '';
    30 if ( empty( $quick['submitted'] ) && empty( $quick['input'] ) && ! empty( $candidates[0]['url'] ) ) {
    31         $velowp_prefill = (string) $candidates[0]['url'];
    32 }
    33 $velowp_quick_input_prefill = (string) ( $quick['input'] ?? $velowp_prefill );
    3425
    3526$velowp_scan_state  = isset( $scan['state'] ) && is_array( $scan['state'] ) ? (array) $scan['state'] : array();
    … …  
    7162                </div>
    7263
    73                 <div class="velowp-topbar__right">
    74                         <button type="button" class="velowp-topbtn" data-velowp-switch="quick"><?php echo esc_html__( 'Quick check', 'veloimg' ); ?></button>
    75                 </div>
    7664        </div>
    7765
    … …  
    8775                        <div class="velowp-card">
    8876                                <h2 class="velowp-h2"><?php echo esc_html__( 'Generation settings', 'veloimg' ); ?></h2>
    89                                 <p class="description"><?php echo esc_html__( 'Configure the parameters first, then run a quick generation check. Only generation is checked here; frontend delivery may still be disabled.', 'veloimg' ); ?></p>
     77                                <p class="description"><?php echo esc_html__( 'Configure generation here. Single-file tests are in Diagnostics on the VeloIMG home page.', 'veloimg' ); ?></p>
    9078
    9179                                <form method="post" action="options.php" class="velowp-settings-form">
    … …  
    113101                                                                        <output class="velowp-range__out" id="velowp-webp-jpeg-quality-output"><?php echo esc_html( (string) $velowp_webp_jpeg_quality ); ?></output>
    114102                                                                </div>
     103                                                                <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div>
    115104                                                                <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality WebP. Lower values reduce file size.', 'veloimg' ); ?></p>
    116105                                                        </td>
    … …  
    137126                                                                        <output class="velowp-range__out" id="velowp-webp-png-quality-output"><?php echo esc_html( (string) $velowp_webp_png_quality ); ?></output>
    138127                                                                </div>
     128                                                                <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div>
    139129                                                                <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality WebP. Lower values reduce file size.', 'veloimg' ); ?></p>
    140130                                                                <p class="description"><?php echo esc_html__( 'These settings will be used by future WebP generation. They do not start conversion by themselves.', 'veloimg' ); ?></p>
    … …  
    212202        </section>
    213203
    214         <!-- QUICK -->
    215         <section class="velowp-panel" data-panel="quick">
    216                 <form method="post" class="velowp-card velowp-quick-card" action="#quick">
    217                         <?php wp_nonce_field( 'velowp_quick', 'velowp_quick_nonce' ); ?>
    218 
    219                         <input type="hidden" name="velowp_quick_input" id="velowp-quick-input" value="<?php echo esc_attr( $velowp_quick_input_prefill ); ?>" />
    220 
    221                         <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Test generation on one file', 'veloimg' ); ?></h3>
    222                         <p class="description"><?php echo esc_html__( 'Pick one image and run a safe test conversion. This helps confirm that WebP generation works before starting bulk processing.', 'veloimg' ); ?></p>
    223 
    224                         <div class="velowp-quick-inline velowp-quick-inline--top">
    225                                 <?php if ( ! empty( $candidates ) ) : ?>
    226                                         <div class="velowp-quick-pick">
    227                                                 <select id="velowp-candidate-select" class="velowp-select velowp-select--wide">
    228                                                         <option value=""><?php echo esc_html__( 'Select', 'veloimg' ); ?></option>
    229                                                         <?php foreach ( (array) $candidates as $velowp_candidate ) : ?>
    230                                                                 <?php
    231                                                                         $velowp_candidate_url  = (string) ( $velowp_candidate['url'] ?? '' );
    232                                                                         $velowp_candidate_size = (int) ( $velowp_candidate['size'] ?? 0 );
    233                                                                         $velowp_width    = (int) ( $velowp_candidate['width'] ?? 0 );
    234                                                                         $velowp_height    = (int) ( $velowp_candidate['height'] ?? 0 );
    235                                                                         $velowp_candidate_label = '';
    236                                                                         if ( ! empty( $velowp_candidate['title'] ) ) {
    237                                                                                 $velowp_candidate_label .= (string) $velowp_candidate['title'] . ' - ';
    238                                                                         }
    239                                                                         if ( $velowp_width > 0 && $velowp_height > 0 ) {
    240                                                                                 $velowp_candidate_label .= $velowp_width . 'x' . $velowp_height;
    241                                                                         }
    242                                                                         if ( $velowp_candidate_size > 0 ) {
    243                                                                                 $velowp_candidate_label .= ' - ' . sprintf( '%.2f MB', $velowp_candidate_size / 1024 / 1024 );
    244                                                                         }
    245                                                                 ?>
    246                                                                 <option value="<?php echo esc_attr( $velowp_candidate_url ); ?>" <?php selected( $velowp_quick_input_prefill, $velowp_candidate_url ); ?>><?php echo esc_html( $velowp_candidate_label ); ?></option>
    247                                                         <?php endforeach; ?>
    248                                                 </select>
    249                                         </div>
    250                                 <?php else : ?>
    251                                         <div class="velowp-quick-pick velowp-quick-pick--fallback">
    252                                                 <input
    253                                                         type="text"
    254                                                         name="velowp_quick_input"
    255                                                         id="velowp-quick-input-fallback"
    256                                                         class="regular-text velowp-quick-input"
    257                                                         value="<?php echo esc_attr( $velowp_quick_input_prefill ); ?>"
    258                                                         placeholder="https://example.com/wp-content/uploads/2025/01/photo.jpg"
    259                                                 />
    260                                         </div>
    261                                 <?php endif; ?>
    262 
    263                                 <div class="velowp-actions velowp-actions--launch velowp-actions--inline">
    264                                         <button type="submit" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-quick-generate" name="velowp_quick_action" value="convert_now" <?php disabled( '' === trim( $velowp_quick_input_prefill ) ); ?>><?php echo esc_html__( 'Generate WebP', 'veloimg' ); ?></button>
    265                                 </div>
    266                         </div>
    267 
    268                         <?php if ( ! empty( $quick['submitted'] ) ) : ?>
    269                                 <hr class="velowp-hr velowp-hr--quick" />
    270                                 <h3 class="velowp-h3 velowp-h3--quick-result"><?php echo esc_html__( 'Result', 'veloimg' ); ?></h3>
    271 
    272                                 <?php
    273                                         $velowp_quick_verdict = isset( $quick['verdict'] ) && is_array( $quick['verdict'] ) ? (array) $quick['verdict'] : array();
    274                                         $velowp_result_kind   = (string) ( $velowp_quick_verdict['result_kind'] ?? '' );
    275                                         $velowp_notice_class  = 'notice-error';
    276                                         $velowp_notice_title  = __( 'WebP check failed.', 'veloimg' );
    277                                         if ( in_array( $velowp_result_kind, array( 'created', 'already_ready' ), true ) ) {
    278                                                 $velowp_notice_class = 'notice-success';
    279                                                 $velowp_notice_title = (string) ( $velowp_quick_verdict['summary'] ?? __( 'WebP was created and passed validation.', 'veloimg' ) );
    280                                         } elseif ( 'skipped' === $velowp_result_kind ) {
    281                                                 $velowp_notice_class = 'notice-warning';
    282                                                 $velowp_notice_title = __( 'WebP was not saved.', 'veloimg' );
    283                                         }
    284                                 ?>
    285                                 <?php if ( ! empty( $velowp_quick_verdict['done'] ) ) : ?>
    286                                         <div class="notice <?php echo esc_attr( $velowp_notice_class ); ?> inline velowp-quick-notice">
    287                                                 <p><strong><?php echo esc_html( $velowp_notice_title ); ?></strong></p>
    288 
    289                                                 <?php if ( 'skipped' === $velowp_result_kind ) : ?>
    290                                                         <p class="description velowp-result-note"><?php echo esc_html__( 'Try another file for testing. Some images are intentionally not saved as WebP when the result would be larger than the original.', 'veloimg' ); ?></p>
    291                                                 <?php endif; ?>
    292                                         </div>
    293                                 <?php endif; ?>
    294 
    295                                 <?php if ( ! empty( $quick['original_rel'] ) ) : ?>
    296                                         <?php
    297                                                 $velowp_derivative_url        = (string) ( $quick['d_expected']['derivative_url'] ?? '' );
    298                                                 $velowp_valid_webp_exists     = ! empty( $quick['verdict']['derivative_bytes'] ) && in_array( $velowp_result_kind, array( 'created', 'already_ready' ), true );
    299                                                 $velowp_derivative_url_exists = $velowp_valid_webp_exists && '' !== $velowp_derivative_url;
    300                                                 $velowp_original_bytes        = (int) ( $quick['verdict']['original_bytes'] ?? 0 );
    301                                                 $velowp_webp_bytes            = (int) ( $quick['verdict']['derivative_bytes'] ?? 0 );
    302                                                 $velowp_saved_bytes           = (int) ( $quick['verdict']['saved_bytes'] ?? 0 );
    303                                                 $velowp_saved_percent         = (int) ( $quick['verdict']['saved_percent'] ?? 0 );
    304                                                 $velowp_file_status           = $velowp_valid_webp_exists ? __( 'Exists and valid', 'veloimg' ) : ( ! empty( $quick['d_expected']['exists'] ) ? __( 'Exists, but is not usable', 'veloimg' ) : __( 'Not created', 'veloimg' ) );
    305                                         ?>
    306                                         <div class="velowp-quick-result-grid">
    307                                                 <div class="velowp-quick-result-row">
    308                                                         <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original (relative path)', 'veloimg' ); ?></div>
    309                                                         <div class="velowp-quick-result-value"><code><?php echo esc_html( (string) $quick['original_rel'] ); ?></code></div>
    310                                                 </div>
    311                                                 <div class="velowp-quick-result-row">
    312                                                         <div class="velowp-quick-result-label"><?php echo esc_html( $velowp_derivative_url_exists ? __( 'WebP URL', 'veloimg' ) : __( 'Expected WebP path', 'veloimg' ) ); ?></div>
    313                                                         <div class="velowp-quick-result-value">
    314                                                                 <?php if ( '' !== $velowp_derivative_url ) : ?>
    315                                                                         <div class="velowp-quick-result-url">
    316                                                                                 <code><?php echo esc_html( $velowp_derivative_url ); ?></code>
    317                                                                                 <?php if ( $velowp_derivative_url_exists ) : ?>
    318                                                                                         <span class="velowp-link-actions"><a class="button button-small velowp-btn-mini-open" href="<?php echo esc_url( $velowp_derivative_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__( 'Open WebP', 'veloimg' ); ?></a></span>
    319                                                                                 <?php endif; ?>
    320                                                                         </div>
    321                                                                         <?php if ( ! $velowp_derivative_url_exists ) : ?>
    322                                                                                 <p class="description velowp-result-note"><?php echo esc_html__( 'The file does not exist yet, so it cannot be opened.', 'veloimg' ); ?></p>
    323                                                                         <?php endif; ?>
    324                                                                 <?php else : ?>
    325                                                                         <code>-</code>
    326                                                                 <?php endif; ?>
    327                                                         </div>
    328                                                 </div>
    329                                                 <div class="velowp-quick-result-row">
    330                                                         <div class="velowp-quick-result-label"><?php echo esc_html__( 'File status', 'veloimg' ); ?></div>
    331                                                         <div class="velowp-quick-result-value"><?php echo esc_html( $velowp_file_status ); ?></div>
    332                                                 </div>
    333                                                 <?php if ( $velowp_original_bytes > 0 ) : ?>
    334                                                         <div class="velowp-quick-result-row">
    335                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original size', 'veloimg' ); ?></div>
    336                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_original_bytes, 2 ) ); ?></div>
    337                                                         </div>
    338                                                 <?php endif; ?>
    339                                                 <?php if ( $velowp_webp_bytes > 0 ) : ?>
    340                                                         <div class="velowp-quick-result-row">
    341                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'WebP size', 'veloimg' ); ?></div>
    342                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_webp_bytes, 2 ) ); ?></div>
    343                                                         </div>
    344                                                 <?php endif; ?>
    345                                                 <?php if ( $velowp_saved_bytes > 0 ) : ?>
    346                                                         <div class="velowp-quick-result-row">
    347                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Savings', 'veloimg' ); ?></div>
    348                                                                 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_saved_bytes, 2 ) . ' (' . $velowp_saved_percent . '%)' ); ?></div>
    349                                                         </div>
    350                                                 <?php endif; ?>
    351                                                 <?php if ( ! empty( $quick['verdict']['reasons'] ) && is_array( $quick['verdict']['reasons'] ) ) : ?>
    352                                                         <div class="velowp-quick-result-row">
    353                                                                 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Reason / details', 'veloimg' ); ?></div>
    354                                                                 <div class="velowp-quick-result-value">
    355                                                                         <ul class="velowp-list">
    356                                                                                 <?php foreach ( (array) $quick['verdict']['reasons'] as $velowp_reason ) : ?>
    357                                                                                         <li><?php echo esc_html( (string) $velowp_reason ); ?></li>
    358                                                                                 <?php endforeach; ?>
    359                                                                         </ul>
    360                                                                 </div>
    361                                                         </div>
    362                                                 <?php endif; ?>
    363                                         </div>
    364                                 <?php endif; ?>
    365                         <?php endif; ?>
    366                 </form>
    367 
    368         </section>
    369204
    370205       
    … …  
    374209                        <div class="velowp-mass-grid velowp-mass-grid--top">
    375210                                <!-- MAIN -->
     211                                <div class="velowp-mass-main-column">
    376212                                <div class="velowp-card velowp-card--glass velowp-mass-card velowp-mass-card--main">
    377213                                        <div class="velowp-mass-card__head">
    378214                                                <div>
    379215                                                        <h2 class="velowp-h2 velowp-h2--tight"><?php echo esc_html__( 'Bulk WebP generation', 'veloimg' ); ?></h2>
    380                                                         <p class="description"><?php echo esc_html__( 'Choose the entire Media Library, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created.', 'veloimg' ); ?></p>
     216                                                        <p class="description"><?php echo esc_html__( 'Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created.', 'veloimg' ); ?></p>
    381217                                                </div>
    382218                                        </div>
    … …  
    385221                                                <label class="velowp-seg">
    386222                                                        <input type="radio" name="velowp_mass_mode" id="velowp-mass-mode-all" value="all" checked />
    387                                                         <span><?php echo esc_html__( 'Entire Media Library', 'veloimg' ); ?></span>
     223                                                        <span><?php echo esc_html__( 'All uploads', 'veloimg' ); ?></span>
    388224                                                </label>
    389225                                                <label class="velowp-seg">
    … …  
    462298                                        </div>
    463299                                        <p class="description" id="velowp-mass-status" aria-live="polite"><?php echo esc_html__( 'Choose a folder to see an estimate and the file list.', 'veloimg' ); ?></p>
     300                                        <?php $velowp_generation_format = 'webp'; require VELOWP_DIR . 'templates/generation-concurrency.php'; ?>
    464301
    465302                                        <div class="velowp-mass-actions velowp-mass-actions--row">
    … …  
    478315                                </div>
    479316
     317                                <?php require VELOWP_DIR . 'templates/webp-retry.php'; ?>
     318                                </div>
    480319                                <!-- SUMMARY -->
    481320                                <div class="velowp-card velowp-card--glass velowp-mass-card velowp-mass-card--summary">
    … …  
    523362
    524363                                        <p id="velowp-statistics-status" class="velowp-muted velowp-hidden" role="status" aria-live="polite"></p>
     364                                        <div class="velowp-table-scroll" tabindex="0" role="region" aria-label="<?php echo esc_attr__( 'Uploads folder statistics', 'veloimg' ); ?>">
    525365                                        <table class="widefat striped velowp-table-tight velowp-folders-table" id="velowp-folders-overview">
    526366                                                <thead>
    … …  
    539379                                                </tbody>
    540380                                        </table>
     381                                        </div>
    541382                                        <p id="velowp-outside-months-summary" class="velowp-muted velowp-hidden" role="status" aria-live="polite"></p>
    542383                                        <p class="velowp-folder-more-wrap"><button type="button" class="button velowp-hidden" id="velowp-folders-more" hidden disabled><?php echo esc_html__( 'Show more', 'veloimg' ); ?></button></p>
  • veloimg/trunk/templates/admin-home.php

    r3728637 r3739629  
    2525}
    2626$velowp_image_strategy_current = isset( $image_strategies[ $image_strategy ] ) && is_array( $image_strategies[ $image_strategy ] ) ? (array) $image_strategies[ $image_strategy ] : array();
    27 $velowp_image_strategy_label   = (string) ( $velowp_image_strategy_current['label'] ?? __( 'Unknown strategy', 'veloimg' ) );
    2827$velowp_image_strategy_desc    = (string) ( $velowp_image_strategy_current['description'] ?? '' );
    2928$velowp_delivery_enabled       = 'off' !== $delivery_method;
    … …  
    102101                                <div data-velowp-home-result="avif"><?php echo \VeloWP\Admin\OptimizationSummary::home_html( 'avif' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shared template escapes each value. ?></div>
    103102                        </div>
    104                         <div class="velowp-home-status-card is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>">
    105                                 <div class="velowp-home-status-card__head">
    106                                         <strong><?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></strong>
    107                                         <span class="velowp-self-test-badge is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"><?php echo esc_html( $delivery_status ); ?></span>
     103                        <div class="velowp-home-status-card is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" data-velowp-delivery-card>
     104                                <div class="velowp-home-status-card__head">
     105                                        <strong id="velowp-delivery-label"><?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></strong>
     106                                        <select class="velowp-self-test-badge velowp-home-badge-select is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" name="velowp_settings[delivery_method]" id="velowp-delivery-method" form="velowp-home-settings-form" aria-labelledby="velowp-delivery-label" aria-describedby="velowp-delivery-description">
     107                                                <?php foreach ( $delivery_labels as $velowp_delivery_key => $velowp_delivery_label ) : ?>
     108                                                        <option value="<?php echo esc_attr( $velowp_delivery_key ); ?>" <?php selected( $delivery_method, $velowp_delivery_key ); ?>><?php echo 'off' === $velowp_delivery_key ? esc_html__( 'Off', 'veloimg' ) : esc_html( (string) $velowp_delivery_label ); ?></option>
     109                                                <?php endforeach; ?>
     110                                        </select>
    108111                                </div>
    109112                                <p><?php echo esc_html__( 'Controls whether generated files are served to visitors.', 'veloimg' ); ?></p>
    110                                 <p class="description">
    111                                         <?php
    112                                         if ( 'php_safe' === $delivery_method ) {
    113                                                 echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' );
    114                                         } elseif ( 'apache' === $delivery_method ) {
    115                                                 echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' );
    116                                         } else {
    117                                                 echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' );
    118                                         }
    119                                         ?>
    120                                 </p>
    121                         </div>
    122                         <div class="velowp-home-status-card velowp-home-status-card--strategy is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>">
    123                                 <div class="velowp-home-status-card__head">
    124                                         <strong><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></strong>
    125                                 </div>
    126                                 <div class="velowp-home-strategy-value is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"><?php echo esc_html( $velowp_image_strategy_label ); ?></div>
    127                                 <p><?php echo '' !== $velowp_image_strategy_desc ? esc_html( $velowp_image_strategy_desc ) : esc_html__( 'No frontend strategy selected.', 'veloimg' ); ?></p>
     113                                <div id="velowp-delivery-description">
     114                                        <p class="description velowp-delivery-method-note" data-velowp-delivery-note="off" <?php echo 'off' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' ); ?></p>
     115                                        <p class="description velowp-delivery-method-note" data-velowp-delivery-note="php_safe" <?php echo 'php_safe' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' ); ?></p>
     116                                        <p class="description velowp-delivery-method-note" data-velowp-delivery-note="apache" <?php echo 'apache' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' ); ?></p>
     117                                </div>
     118                        </div>
     119                        <div class="velowp-home-status-card velowp-home-status-card--strategy is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" data-velowp-delivery-card>
     120                                <div class="velowp-home-status-card__head">
     121                                        <strong id="velowp-strategy-label"><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></strong>
     122                                </div>
     123                                <select class="velowp-home-strategy-value velowp-home-badge-select is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" name="velowp_settings[image_strategy]" id="velowp-image-strategy" form="velowp-home-settings-form" aria-labelledby="velowp-strategy-label" aria-describedby="velowp-strategy-description">
     124                                        <?php foreach ( $image_strategies as $velowp_strategy_key => $velowp_strategy ) : ?>
     125                                                <option value="<?php echo esc_attr( $velowp_strategy_key ); ?>" data-description="<?php echo esc_attr( (string) ( $velowp_strategy['description'] ?? '' ) ); ?>" <?php selected( $image_strategy, $velowp_strategy_key ); ?>><?php echo esc_html( (string) ( $velowp_strategy['label'] ?? '' ) ); ?></option>
     126                                        <?php endforeach; ?>
     127                                </select>
     128                                <p id="velowp-strategy-description"><?php echo '' !== $velowp_image_strategy_desc ? esc_html( $velowp_image_strategy_desc ) : esc_html__( 'No frontend strategy selected.', 'veloimg' ); ?></p>
    128129                                <p class="description"><?php echo esc_html__( 'Format priority is applied only when frontend delivery is enabled.', 'veloimg' ); ?></p>
    129130                        </div>
    130131                </div>
    131132
    132                 <div class="velowp-card velowp-quick-card velowp-self-test-card" id="velowp-self-test">
     133                <form method="post" action="options.php" class="velowp-settings-form" id="velowp-home-settings-form">
     134                        <?php settings_fields( 'velowp_settings_group' ); ?>
     135                        <input type="hidden" name="velowp_settings[_form]" value="home" />
     136
     137                        <?php submit_button( __( 'Save settings', 'veloimg' ) ); ?>
     138                </form>
     139
     140                <details class="velowp-card velowp-quick-card velowp-home-accordion velowp-self-test-card" id="velowp-self-test" <?php echo $auto_self_test ? 'open' : ''; ?>>
     141                        <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check setup', 'veloimg' ); ?></h3></summary>
     142                        <div class="velowp-home-accordion__body">
    133143                        <div class="velowp-self-test-head">
    134144                                <div>
    135                                         <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check setup', 'veloimg' ); ?></h3>
    136145                                        <p class="description"><?php echo esc_html__( 'Run a diagnostic check for generation, storage and the currently selected frontend delivery mode. Original images are not touched and bulk conversion is not started; AVIF checks may create temporary files and delete them immediately.', 'veloimg' ); ?></p>
    137146                                </div>
    … …  
    178187                                <?php endif; ?>
    179188                        </div>
    180                 </div>
    181 
    182                 <div class="velowp-card velowp-quick-card velowp-url-audit-card" id="velowp-url-audit">
    183                         <div class="velowp-self-test-head">
    184                                 <div>
    185                                         <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check frontend URL', 'veloimg' ); ?></h3>
    186                                         <p class="description"><?php echo esc_html__( 'Scan one page from this site and show what visitors actually receive: AVIF, WebP, original fallback, source order or Apache response rewrites.', 'veloimg' ); ?></p>
    187                                 </div>
    188                         </div>
    189                         <div class="velowp-url-audit-form">
    190                                 <input type="url" id="velowp-url-audit-input" class="regular-text" value="<?php echo esc_attr( home_url( '/' ) ); ?>" placeholder="<?php echo esc_attr( home_url( '/' ) ); ?>" />
    191                                 <button type="button" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-url-audit-run" <?php disabled( 'off' === $delivery_method ); ?>><?php echo esc_html__( 'Check page', 'veloimg' ); ?></button>
    192                         </div>
    193                         <p class="description"><?php echo esc_html__( 'Only URLs from the current site are allowed. The check does not start generation and does not change files.', 'veloimg' ); ?></p>
    194                         <p class="description"><?php echo esc_html__( 'If a page cache is active, clear it after changing frontend delivery; cached HTML can keep old markup until the cache expires.', 'veloimg' ); ?></p>
    195                         <div class="velowp-url-audit-result" id="velowp-url-audit-result" aria-live="polite">
    196                                 <p class="description velowp-muted"><?php echo 'off' === $delivery_method ? esc_html__( 'Frontend delivery is disabled. Enable PHP Safe or Apache /.htaccess before running a page delivery check.', 'veloimg' ) : esc_html__( 'No page check has been run yet.', 'veloimg' ); ?></p>
    197                         </div>
    198                 </div>
    199 
    200                 <form method="post" action="options.php" class="velowp-settings-form">
    201                         <?php settings_fields( 'velowp_settings_group' ); ?>
    202                         <input type="hidden" name="velowp_settings[_form]" value="home" />
    203 
    204                         <h3><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></h3>
    205                         <fieldset class="velowp-strategy-options">
    206                                 <?php foreach ( $image_strategies as $velowp_strategy_key => $velowp_strategy ) : ?>
    207                                         <label class="velowp-strategy-option">
    208                                                 <input type="radio" name="velowp_settings[image_strategy]" value="<?php echo esc_attr( $velowp_strategy_key ); ?>" <?php checked( $image_strategy, $velowp_strategy_key ); ?> />
    209                                                 <strong><?php echo esc_html( (string) ( $velowp_strategy['label'] ?? '' ) ); ?></strong>
    210                                                 <br />
    211                                                 <span class="description"><?php echo esc_html( (string) ( $velowp_strategy['description'] ?? '' ) ); ?></span>
    212                                         </label>
    213                                 <?php endforeach; ?>
    214                         </fieldset>
    215 
     189                        </div>
     190                </details>
     191
     192                <details class="velowp-card velowp-quick-card velowp-home-accordion" id="velowp-home-diagnostics" <?php echo $diagnostics_open ? 'open' : ''; ?>>
     193                        <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Diagnostics', 'veloimg' ); ?></h3></summary>
     194                        <div class="velowp-home-accordion__body">
     195                        <div id="velowp-diagnostics">
     196                                <?php require VELOWP_DIR . 'templates/admin-diagnostics.php'; ?>
     197                        </div>
    216198                        <table class="form-table" role="presentation">
    217199                                <tr>
    218                                         <th scope="row"><?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></th>
     200                                        <th scope="row"><label for="velowp-bypass-query"><?php echo esc_html__( 'View original images', 'veloimg' ); ?></label></th>
    219201                                        <td>
    220                                                 <select name="velowp_settings[delivery_method]" id="velowp-delivery-method">
    221                                                         <?php foreach ( $delivery_labels as $velowp_delivery_key => $velowp_delivery_label ) : ?>
    222                                                                 <option value="<?php echo esc_attr( $velowp_delivery_key ); ?>" <?php selected( $delivery_method, $velowp_delivery_key ); ?>><?php echo esc_html( (string) $velowp_delivery_label ); ?></option>
    223                                                         <?php endforeach; ?>
    224                                                 </select>
    225                                                 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="off" <?php echo 'off' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' ); ?></p>
    226                                                 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="php_safe" <?php echo 'php_safe' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' ); ?></p>
    227                                                 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="apache" <?php echo 'apache' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' ); ?></p>
     202                                                <div class="velowp-bypass-copy">
     203                                                        <input type="text" id="velowp-bypass-query" value="<?php echo esc_attr( $bypass_query ); ?>" class="regular-text" readonly aria-describedby="velowp-bypass-help" />
     204                                                        <button type="button" class="button" id="velowp-bypass-copy"><?php echo esc_html__( 'Copy parameter', 'veloimg' ); ?></button>
     205                                                        <span id="velowp-bypass-copy-status" role="status" aria-live="polite" data-success="<?php echo esc_attr__( 'Copied.', 'veloimg' ); ?>" data-fallback="<?php echo esc_attr__( 'Parameter selected. Copy it with Ctrl+C or Command+C.', 'veloimg' ); ?>"></span>
     206                                                </div>
     207                                                <p class="description" id="velowp-bypass-help"><?php echo 'apache' === $delivery_method ? esc_html__( 'Copy the parameter and append it to the JPG/PNG image address to view the original. Saved settings are not changed.', 'veloimg' ) : esc_html__( 'Copy the parameter and append it to the page address to view the original images. Saved settings are not changed.', 'veloimg' ); ?></p>
    228208                                        </td>
    229209                                </tr>
    230210                                <tr>
    231                                         <th scope="row"><?php echo esc_html__( 'Frontend bypass parameter', 'veloimg' ); ?></th>
     211                                        <th scope="row"><label for="velowp-log-level"><?php echo esc_html__( 'Log level', 'veloimg' ); ?></label></th>
    232212                                        <td>
    233                                                 <input type="text" name="velowp_settings[bypass_param]" value="<?php echo esc_attr( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ); ?>" class="regular-text" />
    234                                                 <p class="description"><?php echo esc_html__( 'Add this query parameter to a page URL to temporarily disable VeloIMG frontend delivery for that page load.', 'veloimg' ); ?></p>
    235                                         </td>
    236                                 </tr>
    237                                 <tr>
    238                                         <th scope="row"><?php echo esc_html__( 'Log level', 'veloimg' ); ?></th>
    239                                         <td>
    240                                                 <select name="velowp_settings[log_level]">
     213                                                <select name="velowp_settings[log_level]" id="velowp-log-level" form="velowp-home-settings-form" aria-describedby="velowp-log-help">
    241214                                                        <?php $velowp_log_level = (string) ( $settings['log_level'] ?? 'error' ); ?>
    242215                                                        <option value="error" <?php selected( $velowp_log_level, 'error' ); ?>><?php echo esc_html__( 'Error', 'veloimg' ); ?></option>
    … …  
    244217                                                        <option value="info" <?php selected( $velowp_log_level, 'info' ); ?>><?php echo esc_html__( 'Info', 'veloimg' ); ?></option>
    245218                                                </select>
    246                                                 <p class="description"><?php echo esc_html__( 'Used for diagnostics. Logs are written through error_log() and are visible in wp-content/debug.log when WP_DEBUG_LOG is enabled.', 'veloimg' ); ?></p>
     219                                                <p class="description" id="velowp-log-help"><?php echo esc_html__( 'Controls how much diagnostic detail is written to the server error log. Leave Error for normal use; Warning and Info help investigate problems, for example at support request. If WordPress debug logging is enabled, messages appear in wp-content/debug.log.', 'veloimg' ); ?></p>
    247220                                        </td>
    248221                                </tr>
    249222                        </table>
    250 
    251                         <?php submit_button( __( 'Save settings', 'veloimg' ) ); ?>
    252                 </form>
     223                        </div>
     224                </details>
     225
     226                <details class="velowp-card velowp-quick-card velowp-home-accordion velowp-url-audit-card" id="velowp-url-audit">
     227                        <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check frontend URL', 'veloimg' ); ?></h3></summary>
     228                        <div class="velowp-home-accordion__body">
     229                        <div class="velowp-self-test-head">
     230                                <div>
     231                                        <p class="description"><?php echo esc_html__( 'Scan one page from this site and show what visitors actually receive: AVIF, WebP, original fallback, source order or Apache response rewrites.', 'veloimg' ); ?></p>
     232                                </div>
     233                        </div>
     234                        <div class="velowp-url-audit-form">
     235                                <input type="url" id="velowp-url-audit-input" class="regular-text" value="<?php echo esc_attr( home_url( '/' ) ); ?>" placeholder="<?php echo esc_attr( home_url( '/' ) ); ?>" />
     236                                <button type="button" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-url-audit-run" <?php disabled( 'off' === $delivery_method ); ?>><?php echo esc_html__( 'Check page', 'veloimg' ); ?></button>
     237                        </div>
     238                        <p class="description"><?php echo esc_html__( 'Only URLs from the current site are allowed. The check does not start generation and does not change files.', 'veloimg' ); ?></p>
     239                        <p class="description"><?php echo esc_html__( 'If a page cache is active, clear it after changing frontend delivery; cached HTML can keep old markup until the cache expires.', 'veloimg' ); ?></p>
     240                        <div class="velowp-url-audit-result" id="velowp-url-audit-result" aria-live="polite">
     241                                <p class="description velowp-muted"><?php echo 'off' === $delivery_method ? esc_html__( 'Frontend delivery is disabled. Enable PHP Safe or Apache /.htaccess before running a page delivery check.', 'veloimg' ) : esc_html__( 'No page check has been run yet.', 'veloimg' ); ?></p>
     242                        </div>
     243                        </div>
     244                </details>
    253245
    254246                <p>
  • veloimg/trunk/templates/optimization-home-result.php

    r3728637 r3739629  
    2121        <div data-velowp-home-summary-group aria-busy="<?php echo $velowp_frozen ? 'true' : 'false'; ?>">
    2222                <?php if ( $velowp_global ) : ?>
    23                         <p data-velowp-home-summary="saved"><?php echo esc_html__( 'Bytes saved', 'veloimg' ); ?>: <strong><?php echo esc_html( $velowp_saved_label ); ?></strong></p>
     23                        <p data-velowp-home-summary="saved"><?php echo esc_html__( 'Bytes saved', 'veloimg' ); ?>: <strong class="velowp-savings-value <?php echo $velowp_saved > 0 ? 'is-success' : ( $velowp_saved < 0 ? 'is-warning' : 'is-neutral' ); ?>"><?php echo esc_html( $velowp_saved_label ); ?></strong></p>
    2424                <?php endif; ?>
    2525                <?php if ( isset( $velowp_global['processed'], $velowp_global['ready'], $velowp_global['failed'] ) ) : ?>
  • veloimg/trunk/veloimg.php

    r3728637 r3739629  
    44 * Plugin URI: https://veloimg.ru/
    55 * Description: Generate WebP and AVIF derivatives, manage large media libraries, and deliver optimized images through PHP Safe or Apache/LiteSpeed rules.
    6  * Version: 0.9.6
     6 * Version: 0.9.7
    77 * Requires at least: 6.9
    88 * Requires PHP: 7.4
    … …  
    1818}
    1919
    20 define( 'VELOWP_VERSION', '0.9.6' );
     20define( 'VELOWP_VERSION', '0.9.7' );
    2121define( 'VELOWP_DB_VERSION', '1.5.2' );
    2222define( 'VELOWP_FILE', __FILE__ );
Note: See TracChangeset for help on using the changeset viewer.