| @@ -86,8 +86,12 @@ | ||
| 86 | 86 | return null; |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | 89 | $path = '' === implode( '/', $segments ) ? '/' : '/' . implode( '/', $segments ) . '/'; |
| 90 | + // The tree stores decoded names (Cache::static_path()). Encode them | |
| 91 | + // back the way a permalink is spelled, so the row merges with the | |
| 92 | + // flat entry's canonical URL instead of listing the page twice. | |
| 93 | + $path = Cache::normalize_path( $path ); | |
| 90 | 94 | |
| 91 | 95 | return ( $https ? 'https://' : 'http://' ) . $host . $path; |
| 92 | 96 | } |
| 93 | 97 | |
| @@ -142,9 +146,12 @@ | ||
| 142 | 146 | $merged = array(); |
| 143 | 147 | |
| 144 | 148 | foreach ( $rows as $row ) { |
| 145 | 149 | $url = isset( $row['url'] ) && is_string( $row['url'] ) ? $row['url'] : null; |
| 146 | - $key = null !== $url ? 'u:' . $url : 'p:' . (string) ( $row['path'] ?? '' ); | |
| 150 | + // A flat entry's URL comes from the page's canonical link, which | |
| 151 | + // may carry either escape case; compare in the cache key's one | |
| 152 | + // spelling so a page's flat and static copies stay one row. | |
| 153 | + $key = null !== $url ? 'u:' . Cache::normalize_path( $url ) : 'p:' . (string) ( $row['path'] ?? '' ); | |
| 147 | 154 | |
| 148 | 155 | if ( ! isset( $merged[ $key ] ) ) { |
| 149 | 156 | $merged[ $key ] = $row; |
| 150 | 157 | continue; |
| @@ -214,9 +221,26 @@ | ||
| 214 | 221 | $scanned = 0; |
| 215 | 222 | $capped = false; |
| 216 | 223 | |
| 217 | 224 | // Flat tree — md5-keyed, URL recovered from the markup. |
| 218 | - $flat = defined( 'XSPEED_CACHE_DIR' ) ? (array) glob( XSPEED_CACHE_DIR . '/*.html' ) : array(); | |
| 225 | + // Flat entries live in per-site buckets since #6 | |
| 226 | + // (XSPEED_CACHE_DIR/<host>/<md5>.html); the legacy top-level layout | |
| 227 | + // is still globbed so pre-#6 entries remain visible until they age out. | |
| 228 | + $flat = defined( 'XSPEED_CACHE_DIR' ) | |
| 229 | + ? array_merge( | |
| 230 | + (array) glob( XSPEED_CACHE_DIR . '/*.html' ), | |
| 231 | + (array) glob( XSPEED_CACHE_DIR . '/*/*.html' ) | |
| 232 | + ) | |
| 233 | + : array(); | |
| 234 | + $flat = array_values( | |
| 235 | + array_filter( | |
| 236 | + $flat, | |
| 237 | + static function ( $f ) { | |
| 238 | + // min/ and rest/ are separate buckets, not page entries. | |
| 239 | + return ! in_array( basename( dirname( (string) $f ) ), array( 'min', 'rest', 'combined' ), true ); | |
| 240 | + } | |
| 241 | + ) | |
| 242 | + ); | |
| 219 | 243 | foreach ( $flat as $file ) { |
| 220 | 244 | if ( $scanned >= self::SCAN_CAP ) { |
| 221 | 245 | $capped = true; |
| 222 | 246 | break; |
| @@ -368,16 +392,22 @@ | ||
| 368 | 392 | ), |
| 369 | 393 | ); |
| 370 | 394 | |
| 371 | 395 | if ( defined( 'XSPEED_CACHE_DIR' ) ) { |
| 396 | + // Both layouts: per-site buckets (#6) and pre-#6 top level. | |
| 372 | 397 | self::add_glob( $buckets['pages_flat'], XSPEED_CACHE_DIR . '/*.html' ); |
| 373 | 398 | self::add_glob( $buckets['metadata'], XSPEED_CACHE_DIR . '/*.meta' ); |
| 374 | 399 | self::add_glob( $buckets['precompressed'], XSPEED_CACHE_DIR . '/*.br' ); |
| 400 | + self::add_glob( $buckets['pages_flat'], XSPEED_CACHE_DIR . '/*/*.html' ); | |
| 401 | + self::add_glob( $buckets['metadata'], XSPEED_CACHE_DIR . '/*/*.meta' ); | |
| 402 | + self::add_glob( $buckets['precompressed'], XSPEED_CACHE_DIR . '/*/*.br' ); | |
| 375 | 403 | self::add_glob( $buckets['rest'], XSPEED_CACHE_DIR . '/rest/*.json' ); |
| 376 | 404 | self::add_glob( $buckets['assets'], XSPEED_CACHE_DIR . '/min/*.css' ); |
| 377 | 405 | self::add_glob( $buckets['assets'], XSPEED_CACHE_DIR . '/min/*.js' ); |
| 378 | 406 | self::add_glob( $buckets['assets'], XSPEED_CACHE_DIR . '/min/combined/*.css' ); |
| 379 | 407 | self::add_glob( $buckets['assets'], XSPEED_CACHE_DIR . '/min/combined/*.js' ); |
| 408 | + // One manifest per minified source, per blog (Asset_Manifest). | |
| 409 | + self::add_glob( $buckets['metadata'], XSPEED_CACHE_DIR . '/min/manifests/*/*.json' ); | |
| 380 | 410 | } |
| 381 | 411 | |
| 382 | 412 | if ( defined( 'XSPEED_CACHE_STATIC_DIR' ) && is_dir( XSPEED_CACHE_STATIC_DIR ) ) { |
| 383 | 413 | $static = self::measure_static( XSPEED_CACHE_STATIC_DIR ); |
| @@ -506,18 +536,27 @@ | ||
| 506 | 536 | |
| 507 | 537 | /** |
| 508 | 538 | * The last-purge drill-down. |
| 509 | 539 | * |
| 510 | - * @return array{events:array<int,array<string,mixed>>,last_purge:int} | |
| 540 | + * `last_gc` / `gc_removed` / `gc_removed_total` come from the daily | |
| 541 | + * `xspeed_gc` sweep (Cache_GC) so the collector is verifiable from the | |
| 542 | + * dashboard instead of over SSH. | |
| 543 | + * | |
| 544 | + * @return array{events:array<int,array<string,mixed>>,last_purge:int,last_gc:int,gc_removed:int,gc_removed_total:int,gc_next_run:int} | |
| 511 | 545 | */ |
| 512 | 546 | public static function purge_log( int $limit = 25 ): array { |
| 513 | 547 | $limit = max( 1, min( 50, $limit ) ); |
| 514 | 548 | $events = self::filter_purges( Activity_Log::entries(), $limit ); |
| 515 | 549 | $stats = get_option( 'xspeed_stats', array() ); |
| 550 | + $stats = is_array( $stats ) ? $stats : array(); | |
| 516 | 551 | |
| 517 | 552 | return array( |
| 518 | - 'events' => $events, | |
| 519 | - 'last_purge' => is_array( $stats ) && isset( $stats['last_purge'] ) ? (int) $stats['last_purge'] : 0, | |
| 553 | + 'events' => $events, | |
| 554 | + 'last_purge' => (int) ( $stats['last_purge'] ?? 0 ), | |
| 555 | + 'last_gc' => (int) ( $stats['last_gc'] ?? 0 ), | |
| 556 | + 'gc_removed' => (int) ( $stats['gc_removed'] ?? 0 ), | |
| 557 | + 'gc_removed_total' => (int) ( $stats['gc_removed_total'] ?? 0 ), | |
| 558 | + 'gc_next_run' => (int) wp_next_scheduled( Cache_GC::CRON_HOOK ), | |
| 520 | 559 | ); |
| 521 | 560 | } |
| 522 | 561 | |
| 523 | 562 | /** Drop the memoized scan — called after a purge so the list can't lie. */ |