# Skinbase.org — Optimization Discovery Audit **Audit type:** read-only discovery (no application, schema, or configuration changes) **Date:** 2026-06-05 **Repository:** `D:\Sites\Skinbase26` **Branch:** `develop` **Commit:** `5af95f6533a129e6c22f051c632ab821240e8317` (`5af95f65 Optimize academy`) **Working tree at start:** already dirty (many modified and untracked files). Those files were not reset, stashed, or cleaned. This audit inspected the on-disk tree as it existed, including dirty work. --- ## 1. Executive Summary ```text Backend: Laravel 12.61.1, PHP ^8.2 (composer platform hints 8.4), Horizon 5.47.2, Reverb 1.10.2 Frontend: Hybrid MPA: Blade (Nova homepage/gallery) + Inertia React 19 + Vite 7 SSR Database: MySQL (env example: DB_CONNECTION=mysql, DB_DATABASE=skinbase26); legacy MySQL also configured Cache: default CACHE_STORE=database; homepage uses failover store redis→database→array Sessions: SESSION_DRIVER=database, 120 min; ConditionalStartSession skips anonymous public GET Queues: QUEUE_CONNECTION=redis (.env.example); Horizon + supervisor/systemd examples Search: Laravel Scout 10.25 + Meilisearch PHP 1.16; SQL LIKE fallbacks remain (news search) Storage: local disks + S3-compatible object storage for artworks; originals also on local/backup roots CDN: FILES_CDN_URL / AVATAR_CDN_URL default https://cdn.skinbase.org; Cloudflare purge optional Rendering: Blade for public homepage/browse/search; Inertia+SSR for studio/admin/profile/upload/feed Approx. size: ~1361 PHP in app/, 511 JSX, 248 Blade views, 308 migrations, 376 services, 317 controllers, 205 models Primary content: Public artworks (skins, wallpapers, photography, digital art) plus academy, forum, feed, collections Primary suspected bottleneck areas: 1) High-write counters (views/downloads) and scheduled ranking/metric scans 2) Default database cache/session vs Redis already used for queues 3) Media pipeline (sync derivatives by default) and PHP-streamed downloads unless nginx accel is live 4) Search/filter URL surfaces + sitemap live-build fallback 5) Queue worker timeout 90s vs recommendation jobs timeout 900s Audit confidence: HIGH for architecture/code paths; MEDIUM for production runtime (no live server/.env); HIGH for historical slow-query evidence in docs/slow-query-optimization-plan.md ``` ```text P0 count: 4 P1 count: 16 P2 count: 12 P3 count: 8 ``` This audit does **not** recommend an implementation order. It classifies evidence so a later milestone can design a measured roadmap. --- ## 2. Repository State | Item | Value | | ---- | ----- | | Branch | `develop` (up to date with `origin/develop`) | | HEAD | `5af95f6533a129e6c22f051c632ab821240e8317` | | Last commit | `5af95f65 Optimize academy` | | Working tree before audit | **Dirty** | | Audit modifications to app | **None** | Dirty at start (non-exhaustive; git status at start of audit): - Modified: sitemap pipeline, academy controllers/views, SearchController, Artwork model/observers, vision vector search, composer/package.json, robots.txt, routes, tests, SSR bootstrap, featured thumbnail generator. - Deleted: `public/sitemap.xml` - Untracked: security-report feature, academy pages sitemap builder, featured thumbnail audit migration, nginx search-rate-limit.conf, admin system pages. **Safety:** the audit did not reset, stash, checkout, or revert any of that work. --- ## 3. Architecture ### Architecture map (as coded) ```text Browser ↓ nginx (repo snippets: static-cache, sitemaps, download-accel, search-rate-limit, upstream-error-pages) ├── hashed /build/assets (1y Cache-Control) ├── public/sitemaps/*.xml (try_files then PHP) └── PHP-FPM (unknown production pool) ↓ Laravel 12 ├── MySQL (artworks, users, stats, sessions, cache table, ControlPanel) ├── Redis (queues, Horizon, presence, optional homepage cache, artwork_stats deltas) ├── Meilisearch (Scout indexes: artworks, users, groups, posts, messages) ├── Object storage / CDN (cdn.skinbase.org, S3 disk) ├── Vision / CLIP / YOLO / vector gateway (optional HTTP) ├── Stripe / Cashier (Academy billing; disabled by default in .env.example) ├── Sentry ├── Reverb + Echo (messaging) └── Queue workers (Horizon supervisors + deploy/supervisor example) Node SSR (Inertia) on 127.0.0.1:13714 [config/inertia.php] Python enhance-worker (services/enhance-worker) optional ``` ### Web request lifecycle 1. nginx (if snippets are included in production vhost — **UNKNOWN whether live**). 2. `public/index.php` → Laravel 12 bootstrap. 3. Web middleware replacements: `ConditionalStartSession`, `ConditionalShareErrorsFromSession`, `ConditionalValidateCsrfToken`. 4. Appended: `SecurityHeaders`, `RedirectLegacyProfileSubdomain`, `TrackOnlineVisitor`, `UpdateLastVisit`, `HandleInertiaRequests`, onboarding/email-upgrade middleware. 5. Route → controller/service → Blade **or** Inertia. 6. Guest homepage sets `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600`. Authenticated responses set `private, no-store`. ### Confirmed vs unknown | Layer | Status | | ----- | ------ | | Application architecture | CONFIRMED from repo | | Production nginx/PHP-FPM/OPcache | UNKNOWN — only snippets in `deploy/nginx/` | | Production `.env` drivers | UNKNOWN — `.env.example` only | | Historical MySQL slow log | CONFIRMED in `docs/slow-query-optimization-plan.md` (server3 / db `skinbase`, 2026-04-04→04-26) | --- ## 4. Technology Stack | Area | Evidence | Version / default | | ---- | -------- | ----------------- | | PHP | `composer.json` require | `^8.2`; platform `ext-pcntl/posix` 8.4.0 | | Laravel | `composer.lock` | **v12.61.1** | | Inertia | lock | inertia-laravel **v1.3.4**; JS `@inertiajs/react` ^1.0.4 | | Horizon | lock | **v5.47.2** | | Reverb | lock | **v1.10.2** | | Scout | lock | **v10.25.0** | | Meilisearch PHP | lock | **v1.16.1** | | Intervention Image | lock | **3.11.8** | | Predis | lock | **v3.5.0**; `.env.example` `REDIS_CLIENT=phpredis` | | Sentry | lock | **4.25.1** | | Cashier | composer.json | `^16.5` | | Debugbar | require-dev | fruitcake/laravel-debugbar **v4.3.0** | | Pest | require-dev | `^4.3` | | Node/Vite | package.json | Vite **^7.0.7**, React **^19.2.4**, Tailwind **^3.1.0** (also `@tailwindcss/vite` ^4) | | Package manager | lockfiles | Composer + npm (`package-lock.json`) | | CSS | resources | Tailwind + SCSS `nova.scss` + `nova-grid.css` | | Auth | Breeze-style + Socialite | session guard `web`; extra `controlpanel` guard | | Roles | middleware aliases | admin/moderator/staff/creator | | Mail | `.env.example` | `MAIL_MAILER=log` | | Analytics | code | internal academy/feed/discovery events; no GA package found in composer | | Error monitoring | Sentry Integration in `bootstrap/app.php` | DSN from env | | Realtime | Reverb + Echo + pusher-js | messaging | | ControlPanel | `packages/klevze` | ~1436 PHP files, ~510 `/cp` routes | | Legacy site | `oldSite/` | historical PHP/assets; not the live app | **No Docker Compose / GitHub Actions** found at repo root. --- ## 5. Application Domains Domains that **exist** in code (not assumed): - Artworks / gallery / downloads / views / favourites / likes / reactions / awards / medals - Categories / content types / tags - Users / profiles / avatars / covers / XP / achievements / followers - Discover / explore / ranking / heat / trending / for-you / recommendations / CLIP vectors - Collections / groups / worlds / web stories - Studio / dashboard / upload v1+v2 / enhance - Comments (artwork, profile, news, collection, nova cards, posts) - Forum - Feed / posts / hashtags - News / blog / pages / interviews - Academy (courses, lessons, prompts, packs, challenges, billing) - Nova Cards - Messaging (Reverb) - Search (Meilisearch + SQL) - Sitemaps / robots / SEO - Moderation / staff / traffic online visitors - ControlPanel (`/cp`) - Security reports (untracked at audit time) ### Size inventory (application tree, excluding vendor/node_modules) | Kind | Count | | ---- | ----- | | PHP `app/` | 1361 | | Blade `resources/views` | 248 | | PHP `database/` | 330 | | PHP tests | 274 | | Config PHP | 63 | | JSX | 511 | | JS in `resources/js` | 50 | | Controllers | 317 | | Models | 205 | | Services | 376 | | Jobs | 45 | | Listeners | 6 | | Commands | 124 | | Middleware | 24 | | Policies | 17 | | Events | 33 | | Notifications | 13 | | Observers | 10 | | Repositories | 3 | | Mail | 6 | | HTTP Resources | 2 | | React pages | 207 | | React components | 288 | | Blade components | 29 | | Migrations | 308 | | Feature tests | 227 | | Unit tests | 45 | | Playwright e2e | 11 | | Packages PHP | 1436 | | public/build JS | 260 | | public/build CSS | 5 | --- ## 6. Route Inventory `php artisan route:list --json` succeeded. `var/routes.json` (cached dump) reports **1518** routes. Approximate mix from `var/routes.json`: | Bucket | Approx count | Notes | | ------ | ------------ | ----- | | GET\|HEAD | 770 | | | POST | 572 | | | Auth-ish middleware | 669 | | | `api/*` | 303 | Many still use `web` middleware group | | `cp` ControlPanel | 510 | Dominant admin surface | | `studio` | 155 | | | `settings` | 75 | | | `moderation` / admin | included in admin-ish 569 | | | `_debugbar` | 5 | Present in this dump — debug tooling registered in this environment | | Horizon | 22 | | Source-level `Route::` calls: `web.php` ~660, `api.php` ~272, `auth.php` 27, `legacy.php` 34. ### Important public page types (benchmark candidates) | Page Type | Example Route | Handler | Public? | Cache candidate? | | --------- | ------------- | ------- | ------- | ---------------- | | Homepage | `GET /` | `Web\HomeController@index` | yes | **already** guest flexible cache | | Homepage alias | `GET /home` | same | yes | duplicate URL | | Featured | `GET /featured` | `FeaturedArtworksController` | yes | yes | | Latest | `GET /uploads/latest` | `Community\LatestController` | yes | yes | | Discover trending | `GET /discover/trending` | `DiscoverController@trending` | yes | Meili + Cache::remember | | Discover rising | `GET /discover/rising` | `DiscoverController@rising` | yes | yes | | Discover fresh | `GET /discover/fresh` | `DiscoverController@fresh` | yes | yes | | Explore | `GET /explore` | `ExploreController@index` | yes | Meili TTL map | | Content-type browse | `GET /{contentTypeSlug}/{path?}` | `BrowseGalleryController@content` | yes | versioned cache | | Artwork detail | `GET /art/{id}/{slug?}` | `ArtworkPageController@show` | yes | per-id candidate | | Alternate artwork URL | `GET /{type}/{categoryPath}/{artwork}` | `BrowseGalleryController@showArtwork` | yes | duplicate of `/art/{id}` | | Download | `GET /download/artwork/{id}` | `ArtworkDownloadController` | yes | no (write + file) | | Search | `GET /search` | `Web\SearchController@index` | yes | Meili + news LIKE; noindex | | Tags index | `GET /tags` | `Web\TagController@index` | yes | yes | | Tag show | `GET /tags/{tag}` (two route shapes) | TagController vs HashtagFeed | yes | collision risk | | Categories | `GET /categories` | `CategoryController@index` | yes | yes | | Profile | `GET /@{username}` | `ProfileController@showByUsername` | yes | per-user | | Sitemap | `GET /sitemap.xml` | `SitemapController@index` | yes | static file preferred | | Robots | `GET /robots.txt` | `RobotsTxtController` | yes | generated | | For-you | `GET /discover/for-you` | auth | no | personalized | --- ## 7. High-Traffic Request Paths ### Homepage `GET /` ```text Route / (web) → ConditionalStartSession (skip anonymous GET if configured) → TrackOnlineVisitor (Redis presence) → HomeController::index → HomepageService::all() [guest] or allForUser($user) → view web.home (Blade, not Inertia) ``` - Guest: `Cache::store(homepage failover)->flexible(...)` TTL default 1800s, fresh 30s (`HomepageService`, `config/homepage.php`). - Authenticated: **no payload cache**; personalization + same public rails. - Sections: hero, announcement, community favorites, hall of fame, rising, trending, fresh, collections, world spotlight, groups, tags, creators, news. - Cache-Control set on response (guest public, user no-store). - Obvious DB/Meili sources: many (each section method); mitigated for guests by payload cache. - External APIs: none on this path unless world/news fail internally. - **CONFIRMED cache candidate already implemented for guests.** Authenticated homepage is still a **potential cache candidate** (partial). ### Browse / explore / discover - Discover trending: `ArtworkSearchService::discoverTrending` (Meili cached) with SQL fallback `fallbackTrendingFromDatabase`. - Explore/browse: Meilisearch sort maps + `Cache::remember` keyed by sort/page/viewer segment (`BrowseGalleryController` CACHE_VERSION `v4`). - Content-type catch-all `/{contentTypeSlug}/{path?}` is a high-crawl surface. ### Artwork detail `GET /art/{id}/{slug?}` ```text ArtworkPageController::show → Artwork::withTrashed lookup → 404/410/403 suggestion queries (ErrorSuggestionService) → full with([user.profile, group..., categories..., tags, stats, awardStat]) → 301 if slug mismatch → ArtworkResource (extra Schema::hasTable + per-request counts) → related query with orWhereHas(categories|tags) limit 12 → ArtworkComment::limit(500) with user.profile → Blade/Inertia mix (seo via SeoFactory::artwork) ``` Query sources: 2 artwork loads + related `orWhereHas` + comments up to 500 + resource extras (followers, likes, bookmarks, favourites). View increment is **not** on this GET; client POSTs `/api/art/{id}/view`. ### Profile `GET /@{username}` `ProfileController::renderProfilePage`: artworks pagination, featured join `artwork_features`, favourites, `user_statistics`, social links, plus later tabs. Multiple queries; some eager loads present. ### Search `GET /search` (throttle:search) Meilisearch via `ArtworkSearchService::search` **or** `popular()`. Additional SQL `NewsArticle` `LIKE '%q%'` on title/excerpt/content/meta_title. Groups via `GroupDiscoveryService`. Canonical query 301. `page_robots = noindex,follow`. ### Download `GET /download/artwork/{id}` (throttle:downloads) Resolves original via `ArtworkOriginalFileLocator`; writes `artwork_downloads` row; increments counters (possibly **twice** — `incrementDownloadCountIfAvailable` + `ArtworkStatsService::incrementDownloads(..., defer: false)`). X-Accel-Redirect only if `app.download_accel_enabled`. ### View write `POST /api/art/{id}/view` (throttle:120,1) Inserts `artwork_view_events` **and** `incrementViews(..., defer: false)` (immediate MySQL, not Redis delta). XP award may fire. --- ## 8. Database Architecture **Engine:** MySQL (`DB_CONNECTION=mysql` in `.env.example`). Laravel also defines sqlite/pgsql. Tests use sqlite `:memory:`. **Complexity:** High. 308 migrations. `Schema::create` appears across 90+ migration files; unique table count is well over 100 (artworks, users, academy, collections, groups, worlds, forum, feed, rec, nova cards, ControlPanel, etc.). Exact production table count requires `information_schema`. ### High-traffic / large-growth tables (from code + slow-query doc) | Table | Purpose | Important columns | Indexes (from migrations) | Likely usage | | ----- | ------- | ----------------- | ------------------------- | ------------ | | `artworks` | primary content | user_id, slug, is_public, is_approved, published_at, deleted_at, trending_score_*, visibility, maturity_* | PK; slug; browse `(is_public,is_approved,published_at)`; **batch1** `(deleted_at,is_public,is_approved,published_at,id)` and `(deleted_at,is_public,is_approved,user_id)`; FULLTEXT `(title,description)` | catalog, sitemaps, ranking jobs | | `artwork_stats` | denormalized counters | views, downloads, favorites, windowed cols added later | PK artwork_id | joins on list/detail | | `artwork_metric_snapshots_hourly` | hourly metrics | artwork_id, bucket_hour, counts | unique (artwork_id,bucket_hour); idx_bucket_hour; **batch1 idx_bucket_artwork (bucket_hour,artwork_id)** | rising/heat jobs | | `artwork_view_events` | per-view log | artwork_id, user_id, viewed_at | (inspect later) | insert on every view; prune 90d | | `artwork_downloads` | download log | artwork_id, user_id, ip | | insert on download | | `artwork_favourites` / likes / comments / shares / reactions | engagement | FKs + timestamps | various | counts, rec jobs | | `artwork_tag` / `artwork_category` | pivots | | | filters, sitemaps | | `tags` | taxonomy | usage_count, **artworks_count** (batch1) | idx_tags_artworks_count | popular tags | | `users` / `user_profiles` / `user_statistics` | accounts | | | profiles, toolbars | | `rank_artwork_scores` | ranking | model_version, score_* | batch1 idx_mv_trending/new_hot/best | explore | | `sessions` | session store | | | logged-in + leftover cookies | | `cache` / `cache_locks` | default cache | | | if CACHE_STORE=database | | `jobs` / `failed_jobs` | queues if database driver | payload | | LIKE payload scans historically | | `rec_item_pairs` / `rec_artwork_recs` | recommenders | | | nightly jobs | | `user_discovery_events` | personalization | | | queued ingest | | ControlPanel tables | `/cp` | | | admin | Legacy connection `projekti_old_skinbase` is configured for import, not request path. --- ## 9. Existing Indexes **Artworks (core):** - PK `id` - `user_id` - `slug` (unique dropped later; non-unique index remains) - `is_public`, `is_approved`, `published_at` single-column - compound `idx_artworks_browse (is_public, is_approved, published_at)` - compound `idx_public_approved_published_id (deleted_at, is_public, is_approved, published_at, id)` — **added 2026-04-26 batch1** - compound `idx_public_approved_user_id` - FULLTEXT `artworks_title_description_fulltext` (2026-04-26) - many maturity/feature/group indexes **Snapshots:** unique `(artwork_id, bucket_hour)`; `idx_bucket_hour`; `idx_artwork_bucket`; **batch1 `idx_bucket_artwork (bucket_hour, artwork_id)`**. **Rank scores:** `(model_version, score_trending|new_hot|best)` batch1. **Tags:** `artworks_count` column + index (batch1) to replace correlated `count(*)` subquery. ### Index vs query mismatches (EXPLAIN candidates — do not add yet) 1. **Related artworks on detail** (`ArtworkPageController` ~157–184): `WHERE user_id = ? OR group_id = ? OR whereHas categories OR whereHas tags` + `ORDER BY published_at`. Existing indexes do not match this OR/whereHas pattern. **Candidate requiring EXPLAIN.** 2. **Public count** (`count(*)` on public+approved+published): covering index exists after batch1; still a **counter-cache candidate**. 3. **News search LIKE '%q%'** on `content`: no FULLTEXT observed on news in this pass. **EXPLAIN + FULLTEXT/Meili candidate.** 4. **Profile username** `whereRaw('LOWER(username) = ?')`: functional lookup may skip normal unique index. **EXPLAIN candidate.** 5. **Toolbar subqueries** counting artworks/favourites/notifications per user (cached 30s): not a missing index issue if `user_id` indexed; still correlated. 6. **Historical Q1/Q2 aggregate listing** joining derived fav/comment/share counts: batch1 index helps outer scan; inline derived joins still need EXPLAIN on current ranking commands. 7. **Jobs payload LIKE** (historical AutoTag dedupe): no index possible; verify code no longer scans `jobs.payload`. --- ## 10. Query Findings ### QF-001 — Artwork detail related `orWhereHas` ```text File: app/Http/Controllers/Web/ArtworkPageController.php Line/range: ~157–184 Function: show() Query behavior: related artworks via OR of user_id, group_id, whereHas(categories), whereHas(tags), latest published_at, limit 12 Why expensive: whereHas exists-subqueries + OR prevents single index Evidence: source Confidence: high ``` ### QF-002 — Comments unbounded-ish load ```text File: ArtworkPageController.php Line/range: ~209–215 Query behavior: ArtworkComment where artwork_id, is_approved, order created_at, limit 500 Why expensive: hydrates up to 500 comment+user.profile rows on every detail view Evidence: source Confidence: high ``` ### QF-003 — Search news leading-wildcard LIKE ```text File: app/Http/Controllers/Web/SearchController.php Line/range: ~66–78 Query behavior: NewsArticle title/excerpt/content/meta_title LIKE '%q%' Why expensive: cannot use B-tree; content column scan Evidence: source; public /search is throttled but anonymous Confidence: high ``` ### QF-004 — Meilisearch visibility fallback over-fetch ```text File: app/Services/ArtworkSearchService.php Line/range: search() ~85–106; searchWithThumbnailPreference ~109–128 Query behavior: paginate candidate pool up to 240 then filter in PHP Why expensive: large Meili page + extra round trip when maturity filtering required Evidence: SEARCH_CANDIDATE_POOL_MAX = 240 Confidence: medium ``` ### QF-005 — ArtworkResource extra queries on detail serialize ```text File: app/Http/Resources/ArtworkResource.php Line/range: ~23–80+ Query behavior: loadMissing relations; Schema::hasTable; user_statistics lookup; exists() on likes/bookmarks/favourites Why expensive: several extra queries per detail; Schema::hasTable on hot path Evidence: source Confidence: high for detail; would be N+1 if used in lists (list uses ArtworkListResource) ``` ### QF-006 — Guest homepage aggregation (mitigated) ```text File: app/Services/HomepageService.php Function: buildGuestPayload / all Query behavior: many section queries Why expensive on miss: large fan-out Evidence: flexible cache + homepage:warm-guest-cache schedule Confidence: high that miss is expensive; guest hit is mitigated ``` ### QF-007 — Authenticated homepage uncached ```text File: HomepageService::allForUser Why expensive: repeats public rails + user_data counts + for_you + following Evidence: no Cache::remember wrapping allForUser (only some subcalls) Confidence: high ``` ### QF-008 — Toolbar correlated counts ```text File: app/Providers/AppServiceProvider.php View::composer layouts.nova Line/range: ~226–258 Query behavior: selectSub COUNT artworks, favourites, unread notifications + messages join Cached: Cache::remember toolbar:{userId} TTL config toolbar.cache_ttl_seconds default 30 Confidence: high ``` ### QF-009 — Historical production slow log (server3) `docs/slow-query-optimization-plan.md`: 68,950 slow queries / 22 days / 15.39B rows examined. Top class: artwork aggregate SELECT with derived joins (~117,834s, 78% of slow time). Batch1 indexes and tag `artworks_count` were added in repo **the same day as that analysis**. **Production re-measure required** to know residual cost. ### QF-010 — `inRandomOrder()` Interview/user controllers (`InterviewController`, `UserController`). Low traffic vs gallery. Confidence: medium, likely P3. ### QF-011 — Ranking/heat scheduled scans Commands `nova:recalculate-rankings`, `nova:metrics-snapshot-hourly`, `nova:recalculate-heat`, `skinbase:recalculate-trending` every 15–30 minutes. Historical Q1/Q2 look like this class. Confidence: high they are heavy; medium they still match old SQL. --- ## 11. N+1 Findings | Source | Relation | Amplification | Eager load? | | ------ | -------- | ------------- | ----------- | | Gallery mapping using `$a->user?->profile` after `loadMissing(['user.profile','categories.contentType'])` | user/profile/categories | none if loadMissing ran | **present** on search/tag/discover | | `ArtworkListResource` `$user?->profile?->avatar_url` | profile | 1 per card if profile not loaded | list resource checks `relationLoaded('user')` but may still lazy-load profile | | ArtworkResource `contributors.user.profile` | nested | per contributor | loadMissing on detail only | | Comments loop uses `$c->user` | user.profile | 500 max | **with('user.profile') present** | | 404 `ErrorSuggestionService` trending artworks/tags/creators | various | extra queries on 404 | unknown eager | | ControlPanel / DataTables | unknown | `/cp` 510 routes | not fully traced | | Nova toolbar content types | ContentTypeSlugResolver | every Blade nova layout | cached-ish via resolver | No smoking-gun `foreach ($items as $item) { $item->user }` without eager load was confirmed on the main gallery path after `loadMissing`. Residual N+1 risk is **profile avatar accessors** and **ControlPanel**. Confidence: medium. --- ## 12. Cache Architecture | Item | Value | | ---- | ----- | | Default store | `env('CACHE_STORE', 'database')` — **database in .env.example** | | Redis store | defined; homepage failover `redis, database, array` | | File store | `storage/framework/cache/data` | | Tags | not a first-class pattern in app Cache:: calls reviewed | | Key examples | `homepage.payload.guest`, `homepage.tags.{n}`, `search.popular.{segment}.page.{n}`, `toolbar:{userId}`, `explore.cache.version`, `search.related.{id}.{segment}` | | TTL | 120–1800s typical; toolbar 30s; search 300s | | Invalidation | ArtworkObserver increments `explore.cache.version`; homepage clear methods; some forget() on world stories | | Production Redis for default cache | **UNKNOWN** | **Potential cache candidates (not implemented as a change here):** - Authenticated homepage modules - Artwork detail related + comment trees - Public artwork count - Tag directory stats - Error-page suggestion rails - Ranking top-N lists (partially via RankBuildListsJob) - Category directory - `/search` empty-state popular (already remembered) --- ## 13. Session Architecture | Item | Value | | ---- | ----- | | Driver | `database` (`.env.example` / `config/session.php`) | | Table | `sessions` | | Lifetime | 120 minutes | | Lottery | 2/100 sweeps | | Conditional sessions | `config/skinbase-sessions.php` enabled; skip anonymous public GET; skip bots | | Always-session paths | login, dashboard, studio, upload, admin, messages, etc. | | Public skip list | `/`, featured, discover, explore, art, tags, sitemaps, news, … | If a session cookie already exists, session is **not** skipped even on public GET. That limits anonymous CDN/full-page cache for returning users. `HandleInertiaRequests` avoids reading auth when session skipped. **Impact:** full-page HTTP cache for anonymous first-time crawlers is feasible; cookie holders still hit origin. --- ## 14. Queue Architecture | Item | Value | | ---- | ----- | | Default connection | `.env.example` **redis**; config fallback `database` | | Horizon | name `skinbase-nova`; path `/horizon`; prefix `skinbase_nova_horizon:` | | Horizon supervisors | `search,default` (timeout **960s**, production maxProcesses 5); `broadcasts,notifications` (timeout 90s, maxProcesses 3) | | Supervisor example | `--timeout=90` queues `search,forum-security,forum-moderation,vision,recommendations,discovery,mail,default` **numprocs=1** | | systemd example | same `--timeout=90` | | Job timeout example | `RecComputeSimilarHybridJob` **$timeout = 900**, `$tries = 1` | **CONFIRMED mismatch:** deploy worker timeout 90s vs recommendation jobs 900s vs Horizon 960s. If production uses supervisor/systemd snippet rather than Horizon, 900s jobs will be killed. Horizon **does not** list `vision`, `recommendations`, `discovery`, `mail`, `forum-*` as dedicated supervisors; those names only appear on the supervisor example. If Horizon is the production worker, those queues must be consumed via `default` or they stall. `docs/QUEUE.md` already warns Scout `search` queue must be consumed. Job classes include: AutoTag, embeddings/vectors, derivatives, featured thumbs, IncrementArtworkView (queued variant exists but view controller uses sync increment), indexing, discovery ingest, rec compute, sitemaps, enhance, security report, nova cards, leaderboards, etc. Failed jobs: Laravel `failed_jobs` table; Horizon trim failed 10080 minutes. --- ## 15. Scheduler Defined in `routes/console.php` (Laravel 11+ style). `app/Console/Kernel.php` registers commands; **schedules live in console.php**. | Task | Frequency | Purpose | Potential cost | | ---- | --------- | ------- | -------------- | | `skinbase:recalculate-trending` 24h | :06/:36 | trending scores | HIGH (historical slow SQL class) | | trending 7d | :19/:49 | | HIGH | | reset-windowed-stats | daily/weekly 03:30/03:50 | counters | MEDIUM–HIGH | | uploads/enhance cleanup | daily 03:00–03:30 | files | MEDIUM | | analytics aggregates | 03:10–03:35 | discovery/feed/tags | MEDIUM | | academy analytics | hourly + daily | | MEDIUM | | `skinbase:flush-redis-stats` | every 10 min | drain deltas | LOW–MED (if views still sync, less used) | | prune view events | Sunday 04:00 | delete old events | HIGH I/O if table large | | rec pair/tag/behavior/hybrid jobs | 4h / 02:00–02:30 | similarity | HIGH (900s timeouts) | | publish-scheduled posts/artworks/news/cards | every minute | publish | LOW–MED | | collection lifecycle | every 10 min | | MED | | homepage:warm-guest-cache | every 10 min offset | warm cache | MED on miss | | artworks:search-reconcile | hourly :28 | Meili drift | MED | | posts:warm-trending | every 2 min | | LOW–MED | | nova:recalculate-rankings | :07/:37 | ranking v2 | HIGH | | metrics-snapshot-hourly | :02 | snapshots | HIGH write | | nova:recalculate-heat | every 15 min | heat | HIGH | | sitemaps generate | 10:30 and 22:30 | XML | HIGH | | sitemaps publish | every 6h :08 | | MED | | sitemaps validate | 04:45 | | MED | | security:scan | weekly Mon 05:15 | | MED | | leaderboards / rank lists / nova card caches | hourly | | MED | | prune metric snapshots keep 7d | daily 04:00 | | HIGH I/O | | forum AI/bot/post/firewall scans | hourly | | MED | | health:tick | every minute | | LOW | | horizon:snapshot | hourly :45 | | LOW | Night window 02:00–05:00 is densely packed. --- ## 16. Search Architecture ```text /search?q= → throttle:search (20/user/min, 30/IP/min) → canonical 301 → Artwork::search() Scout/Meilisearch → optional PHP visibility filter / candidate pool → NewsArticle LIKE '%q%' → GroupDiscoveryService → Blade search.index, robots noindex,follow ``` - Driver default **meilisearch**. Queue: `search` on Redis. - Indexes: artworks (rich filterable/sortable attrs), messages, plus Searchable on User, Group, Post. - Autocomplete: React `SearchBar.jsx` / overlay (separate API; not fully traced). - Image/vector search: `/api/art/{id}/similar-ai`, `/api/search/image` with `throttle:vector-search`; nginx snippet `search-rate-limit.conf`. - Fallback SQL LIKE remains for news and historical artwork LIKE (slow-query doc); Scout path is primary for artworks. - Scale constraint: Meilisearch cluster size **UNKNOWN**; PHP LIKE on news `content` will not scale. - Search is noindex (good) but `robots.txt` still `Allow: /` so crawlers may hit it until they honor robots meta. --- ## 17. Statistics / Counters | Signal | Mechanism | Sync? | Location | | ------ | --------- | ----- | -------- | | Artwork views | insert `artwork_view_events` + increment `artwork_stats` views/24h/7d | **sync, defer:false** | `ArtworkViewController`, `ArtworkStatsService` | | Queued IncrementArtworkView | exists | not used by this controller | `app/Jobs/IncrementArtworkView.php` | | Redis deltas | `artwork_stats:deltas` + `skinbase:flush-redis-stats` | unused when defer=false | ArtworkStatsService | | Downloads | row in `artwork_downloads` + increment (possibly duplicated) | sync | `ArtworkDownloadController` | | Favourites/likes/comments | tables + observers | write path | observers/jobs medal stats | | Ranking/heat | scheduled recompute | async | commands | | XP | XPService on view | sync on view POST | | | Presence | Redis | after response | TrackOnlineVisitor | | Profile views | not confirmed as a dedicated counter | | | **High-write path (P0/P1):** every public artwork view POST = at least one INSERT + one UPDATE, throttle 120/min/IP, CSRF excepted for `api/art/*/view`. --- ## 18. Media Pipeline | Item | Actual | | ---- | ------ | | Upload max | 50 MB image, 200 MB archive (`config/uploads.php`) | | MIME | jpeg/png/webp; GIF off by default | | Derivatives | xs 320, sm 680, md 1024, lg 1920, xl 2560, sq 512 | | Featured variants | mobile_xs 400 … desktop_xl 2200×1238 | | Quality | 85 default; sq 82 | | Queue derivatives | `UPLOAD_QUEUE_DERIVATIVES` **default false** → **synchronous** publish pipeline unless enabled | | GenerateDerivativesJob | exists; used when queued | | Presenter widths | xs 160 / sm 320 / md 640 / lg 1280 / xl 1920 / sq 400 — **mismatch vs derivative max sizes** | | Format | WebP thumbs typical; originals jpeg/png/webp | | AVIF | not a first-class upload MIME | | CDN | `https://cdn.skinbase.org`; missing thumbs `https://files.skinbase.org/default` | | srcset | ThumbnailPresenter builds srcset; Blade galleries pass `thumb_srcset`; homepage hero preload + fetchpriority high | | lazy | widespread `loading="lazy"`; homepage hero is preloaded (good LCP) | | Lighthouse (skinbase.top, 2026-03-23) | LCP 0.8s, FCP 0.7s, CLS 0.017, TBT 0ms, performance 0.99 — **one historical homepage lab run**, not production skinbase.org proof | --- ## 19. Filesystem / CDN Disks (`config/filesystems.php`): `local` (private), `public`, `sitemaps_public` (public_path), `s3`. Artwork roots (`config/uploads.php`): - `storage/app/artworks` - local originals `storage/app/originals/artworks` - readonly backup `/opt/www/virtual/files/cdn/artworks/original` - object disk `s3` prefix `artworks` Download may stream through PHP if accel disabled (`deploy/nginx/download-accel.conf` documents FPM buffering problem). CDN purge webhook optional (`CDN_PURGE_URL`, Cloudflare zone/token). --- ## 20. Frontend Architecture Hybrid: - **Blade Nova MPA** for homepage, discover, explore, search, many public galleries. - **Inertia + React 19** for studio, admin, profile, upload, feed, forum, academy pages, collections manage. - **SSR** enabled (`config/inertia.php` `ssr.enabled=true`, url `http://127.0.0.1:13714`); `deploy/supervisor/skinbase-ssr.conf` runs `bootstrap/ssr/ssr.js`. Homepage excluded from SSR graph (`resources/js/ssr.jsx`). - Vite 7, Tailwind 3 (+ vite v4 plugin present), Sass, Alpine in devDependencies. - Notable JS: TipTap, highlight.js/lowlight, emoji-mart + data, framer-motion, Echo/pusher-js, marked, react-markdown, turndown. --- ## 21. Frontend Payload Findings | Candidate | Evidence | | --------- | -------- | | Guest homepage JSON-ish `props` | HomepageService serializes many rails (10 items × many sections + collections + tags + creators + news). Cached. | | Authenticated homepage | same plus for_you / following / user_data — uncached | | Artwork detail | ArtworkResource large graph + related 12 + comments ≤500 | | Profile page | artworks page + featured + favourites + stats in one Inertia render | | Shared Inertia | auth user flags + `studio_groups` via GroupService for every Inertia page when logged in (`HandleInertiaRequests::share`) | | Search | full paginator + news + groups | | TipTap / emoji-data chunks | 481 KB + 423 KB — studio/editor, not homepage | `ArtworkListResource` is relatively slim (good). Detail resource is fat (expected). --- ## 22. Build Assets Existing `public/build` inspected (build **not** re-run, to avoid dirtying tree). | Metric | Value | | ------ | ----- | | JS files | 260, **5.73 MB** total | | CSS files | 5, **500 KB** total | | Largest JS | `vendor-tiptap` 481 KB; `emoji-data` 423 KB; `vendor-syntax` 307 KB | | Largest CSS | `app-oMtr0NO-.css` 429 KB | | Manual chunks | tiptap, syntax, tooltip, motion, realtime, emoji (vite.config.mjs) | `npm run build` was **not** executed (node_modules present; existing hashed assets sufficient for size evidence). --- ## 23. SEO Shared system: `config/seo.php`, `SeoFactory` / `SeoDataBuilder`, Blade `partials/seo/head.blade.php`, Inertia `SeoHead.jsx` (`docs/seo-audit-system-v1-summary.md`). | Page type | Title | Description | Canonical | Indexability | Structured data | Potential issue | | --------- | ----- | ----------- | --------- | ------------ | --------------- | --------------- | | Homepage | Skinbase – Digital Art & Wallpapers | yes | `url('/')` | index,follow | WebSite + SearchAction | `/home` duplicate | | Artwork | `{title} by {author} — Skinbase` | stripped desc | `route('art.show')` 301 slug | index | ImageObject + CreativeWork | second URL via content-type path | | Discover | page_title set | yes | canonicalRoute | index | via unified seo if wired | sort/pagination params | | Search | Search: q | yes | canonicalized query | **noindex,follow** | | still Allow:/ in robots | | Tags | Browse Tags | yes | tags.index | index | | two `/tags/{tag}` route patterns | | Profile | via SeoFactory | | lowercase username 301 | index | | `tab=` redirected to path | | 404 | errors layout | | | **noindex,nofollow** | | extra suggestion queries | OG/Twitter: `summary_large_image`; fallback `/gfx/skinbase_back_001.webp`. --- ## 24. Canonicals / Duplicate URLs Flagged surfaces (no redirects implemented in this audit): | Surface | Evidence | | ------- | -------- | | `/` and `/home` | both HomeController | | `/art/{id}/{slug}` vs `/{type}/{category}/{artwork}` | both public artwork URLs | | `/explore/top-rated` 301 to `explore?sort=` | good | | `/discover` 301 to `/discover/trending` | good | | Search extra params 301 stripped | good | | Profile case / username_redirects | 301 | | `/contact` defined twice | PageController marketing **and** ApplicationController `contact.show` (later wins in Laravel) | | `/tags/{tag}` vs feed hashtag regex | possible overlap | | Explore sort querystrings | multiple indexable sorts unless canonicalized | | Legacy `gallery.php`, `/lost-password` | 301 | | www/HTTPS | not in app; **server UNKNOWN** | | `public/robots.txt` vs dynamic `RobotsTxtController` | both exist; route `GET /robots.txt` uses controller | --- ## 25. Sitemaps - Routes: `/sitemap.xml`, `/sitemaps/{name}.xml` - Config: shard size 10k; many families (artworks, users, tags, academy-*, news-google, forum, …) - Scheduler: generate 10:30/22:30, publish every 6h, validate daily, cleanup job - nginx snippet serves static files first - **Defaults:** `SITEMAPS_BUILD_ON_REQUEST=true`, `SITEMAPS_FALLBACK_TO_LIVE_BUILD=true` — if static/published missing, **PHP builds sitemap on crawler request** - `public/sitemap.xml` is **deleted in dirty tree**; static files under `public/sitemaps/` exist - Cache-Control on PHP/file responses uses `sitemaps.cache_ttl_seconds` default 900; nginx snippet 6h Potentially expensive: live-build of sharded artworks sitemap on cache miss. --- ## 26. robots.txt Controller output: ```text User-agent: * Allow: / Sitemap: {APP_URL}/sitemap.xml ``` Static `public/robots.txt` matches (sitemap hard-coded `https://skinbase.org/sitemap.xml`). - No `Disallow` for `/search`, `/explore?*`, `/cp`, `/studio`, `/horizon`, APIs - Relies on meta robots / auth for private areas - Parameter crawl explosion: **POSSIBLE** for sort/filter/pagination - Search is noindex in HTML (good) but still allowed --- ## 27. Web Server / HTTP Repo provides **snippets only**, not a full production vhost: - `deploy/nginx/static-cache.conf` — 1y hashed assets; 7d `/images` - `sitemaps.conf` — try_files then PHP - `download-accel.conf` — internal X-Accel - `search-rate-limit.conf` — untracked in dirty tree; 20r/m search, 10r/m AI search - `upstream-error-pages.conf` — 502/504 static HTML **Production web-server configuration not available in repository** (no live `nginx.conf`, TLS, HTTP/2/3, Brotli, gzip, FPM socket, `client_max_body_size`). App security headers middleware: X-Frame-Options SAMEORIGIN, nosniff, Referrer-Policy, Permissions-Policy. CSP not set here. --- ## 28. PHP Runtime | Item | Repo evidence | | ---- | ------------- | | Version | ^8.2; platform 8.4 extensions | | OPcache / memory_limit / FPM | **not in repo** | | CLI | 124 commands; Horizon memory 128MB workers; master 64MB | | Upload | app 50MB; PHP `upload_max_filesize` UNKNOWN | --- ## 29. Dependencies **Performance-relevant:** Horizon, Scout/Meilisearch, Redis/predis, Intervention Image, Flysystem S3, Reverb. **Image:** intervention/image, gumlet/php-image-resize. **Debug:** fruitcake/laravel-debugbar (require-dev). `bootstrap/cache/packages.php` currently lists debugbar — **this local environment has it discovered**. Production `composer install --no-dev` (docs/deployment.md) should omit it. `var/routes.json` includes `_debugbar` routes. **composer audit (read-only):** advisories reported for transitive `guzzlehttp/guzzle` (9), `guzzlehttp/psr7` (2), `league/commonmark` (6), `mtdowling/jmespath.php` (1), `phpseclib/phpseclib` (1). Abandoned: none. **Do not treat this as a full CVE triage; versions/severity need human review. No secrets printed.** **npm audit:** not completed in this pass. --- ## 30. Security / Abuse-Sensitive Paths Not a pentest. Performance-adjacent: | Path | Control | Residual | | ---- | ------- | -------- | | `/search` | throttle 20/30 per min; query param cap 15 / 500 chars | still Meili+SQL LIKE | | `/api/art/{id}/view` | 120/min; CSRF excepted | cheap to write DB | | `/api/art/{id}/similar-ai` | vector-search limiter; nginx snippet | outbound vision HTTP | | Downloads | 60/120 per min | originals via PHP if no accel | | Uploads | per-user/IP chunk limits; draft quotas | sync image work if queue flag false | | Forum | dedicated firewall/bot/AI middleware | scheduled scans | | Registration | Turnstile optional, disposable domains, rate limits | | | Horizon | `web` middleware only in config — **auth gating UNKNOWN** | | `/cp` | ControlPanel package | large attack/admin surface | | Stripe webhooks | CSRF except `stripe/*` | | --- ## 31. Error Handling `bootstrap/app.php`: Sentry; 404 → `ErrorController::handleNotFound` (pattern-specific suggestion queries); ModelNotFound same; 403/other HTTP → Blade `errors.{status}` or `errors.http`; 500 logs correlation id via `NotFoundLogger` unless `APP_DEBUG`. 404 pages run extra DB (trending artworks, tags, creators). Confidence: medium they are cached internally (not verified). JSON/Inertia 404 returns minimal JSON. --- ## 32. Tests Inventory: Pest Feature 227 + Unit 45 + Playwright e2e 11 + Vitest script. `php artisan test` was started. Partial results before log truncation: - Many unit tests PASS (academy, collections, discovery, early growth, enhance, sanitizer, …) - FAIL observed: - `Tests\Unit\ForumRateLimitRouteTest` - `Tests\Unit\ForumRestrictedCategoryAccessTest` (2) - `Tests\Unit\HomepageAnnouncementModuleTest` (2: homepage payload/render) **Full suite did not finish in the captured log** (output truncated; duration incomplete). Tests were **not** modified to pass. Playwright / Vitest / `npm run build` not re-run. --- ## 33. Existing Performance Tooling | Artifact | What it is | | -------- | ---------- | | `docs/slow-query-optimization-plan.md` | Production slow.log analysis + index plan | | `database/migrations/2026_04_26_082019_add_performance_indexes_batch1.php` | Indexes from that plan | | `lighthouse_metrics.json` | Lighthouse 13 lab run vs **https://skinbase.top/** 2026-03-23 | | `scripts/parse-lighthouse.js`, `parse-lcp.cjs` | parsers | | Debugbar storage `storage/debugbar/*.json` | local query traces | | Horizon metrics snapshots | scheduled | | Playwright e2e | functional, not load | | No k6/ab/wrk/Artillery found | | --- ## 34. Optimization Opportunity Matrix | ID | Area | Finding | Evidence | Impact | Confidence | Priority | Production measurement needed | | -- | ---- | ------- | -------- | ------ | ---------- | -------- | ----------------------------- | | DB-001 | DB | Historical 78% slow time on artwork aggregate listing/joins | slow-query doc Q1/Q2 | very high | high historical; residual UNKNOWN | P0 | EXPLAIN current ranking SQL; table sizes | | DB-002 | DB | Detail related `orWhereHas` OR query | ArtworkPageController | high TTFB on detail | high | P1 | EXPLAIN + p95 art.show | | DB-003 | DB | Comments load 500 | ArtworkPageController | payload/TTFB | high | P1 | p95 comment counts | | DB-004 | DB | News `LIKE %q%` on search | SearchController | search tail latency | high | P1 | slow log / EXPLAIN | | DB-005 | DB | `LOWER(username)` profile lookup | ProfileController | profile TTFB | medium | P2 | EXPLAIN | | CACHE-001 | Cache | Default CACHE_STORE=database | .env.example / config/cache.php | global | medium (prod unknown) | P1 | prod CACHE_STORE, cache table size | | CACHE-002 | Cache | Authenticated homepage uncached | HomepageService::allForUser | TTFB logged-in | high | P1 | logged-in homepage trace | | QUEUE-001 | Queue | Worker timeout 90s vs jobs 900s | deploy supervisor/systemd vs RecComputeSimilar* | failed jobs / stuck recs | high | P0 | which worker actually runs in prod | | QUEUE-002 | Queue | Horizon supervisors omit vision/discovery/mail names | horizon.php vs QUEUE.md | stalled AI jobs | medium | P1 | Horizon dashboard queues | | STAT-001 | Stats | View POST writes event+counter sync | ArtworkViewController defer:false | write amplification | high | P0 | views/sec, innodb row ops | | STAT-002 | Stats | Download may double-increment | ArtworkDownloadController | incorrect + extra writes | medium | P1 | code path test + QPS | | SITEMAP-001 | SEO/Infra | Live-build fallback on request | config/sitemaps.php defaults | crawler-triggered heavy PHP | high | P0 | whether static files always present | | SEO-001 | SEO | robots Allow:/ no Disallow search/filters | RobotsTxtController | crawl budget | medium | P1 | GSC crawl stats | | SEO-002 | SEO | Duplicate artwork URLs | art.show vs browse showArtwork | ranking dilution | high | P1 | GSC duplicates | | MEDIA-001 | Media | Derivatives sync by default | UPLOAD_QUEUE_DERIVATIVES=false | upload latency / FPM | high | P1 | upload p95 | | MEDIA-002 | Media | Presenter sizes ≠ derivative sizes | ThumbnailPresenter vs uploads.php | wrong srcset / overfetch | high | P2 | RUM image bytes | | MEDIA-003 | Media | PHP download without accel | download-accel + controller | FPM blocked on large files | high if accel off | P1 | confirm DOWNLOAD_ACCEL | | SEARCH-001 | Search | Meili candidate pool 240 | ArtworkSearchService | extra Meili/PHP work | medium | P2 | Meili latency | | SEARCH-002 | Search | nginx search limiter is snippet/untracked | search-rate-limit.conf | FPM flood | medium | P1 | whether included in vhost | | FE-001 | Frontend | Large CSS 429KB + editor chunks | public/build | studio CWV not homepage | high | P2 | page-type coverage | | FE-002 | Frontend | Inertia share studio_groups | HandleInertiaRequests | extra query logged-in | medium | P2 | Inertia traces | | INFRA-001 | Infra | Production nginx/FPM/OPcache unknown | deploy snippets only | — | — | P0 data | collect server config | | SEC-001 | Security | composer audit advisories | composer audit | supply chain | medium | P2 | version pin review | | SEC-002 | Security | Debugbar discovered locally; _debugbar routes in dump | packages.php, routes.json | must not be in prod | medium | P1 | prod composer --no-dev | | ERR-001 | Errors | 404 runs suggestion queries | ErrorController | bot 404 load | medium | P2 | 404 QPS | --- ## 35. P0 Findings 1. **QUEUE-001** — Deployed example workers use `--timeout=90` while recommendation jobs declare `$timeout = 900`. Horizon allows 960s. CONFIRMED in repo; production worker type UNKNOWN. 2. **STAT-001** — Artwork views persist an event row and increment MySQL counters on the request (`defer: false`), CSRF-excepted, 120/min. CONFIRMED. 3. **SITEMAP-001** — Config defaults rebuild sitemaps in-request if published/static missing. Dirty tree deleted `public/sitemap.xml`. CONFIRMED default; production file presence UNKNOWN. 4. **DB-001** — Production slow log (Apr 2026) showed artwork aggregate scans as ~78% of slow time. Batch1 indexes exist in migrations; **residual production cost UNKNOWN / requires re-EXPLAIN**. Treated P0 until measured clear. --- ## 36. P1 Findings CACHE-001, CACHE-002, QUEUE-002, STAT-002, SEO-001, SEO-002, MEDIA-001, MEDIA-003, SEARCH-002, SEC-002, DB-002, DB-003, DB-004, plus: - TrackOnlineVisitor Redis write after almost every public GET. - Toolbar correlated counts every 30s per user on Nova layouts. - Historical jobs-table LIKE dedupe pattern (verify gone). - Download original serving vs CDN derivatives. --- ## 37. P2 Findings DB-005, MEDIA-002, SEARCH-001, FE-001, FE-002, ERR-001, SEC-001, inRandomOrder interviews, comment/N+1 residuals, presenter vs files.skinbase.org fallback host mismatch vs cdn.skinbase.org, duplicate `/contact` routes, packed 02:00–05:00 scheduler, academy/forum hourly jobs, Lighthouse only for skinbase.top. --- ## 38. P3 Findings - gumlet/php-image-resize alongside Intervention. - Tailwind v3 + v4 vite plugin coexistence. - Default Laravel README still in repo. - `oldSite/` large binary tree in repository. - Debugbar JSON in `storage/debugbar` (local clutter). - yajra/datatables unbounded version `*`. - jenssegers/agent `*`. - ControlPanel menu composer cost not profiled. --- ## 39. Risky Areas | Area | Why sensitive | | ---- | ------------- | | Ranking / heat / trending SQL | historically the slow-log majority; changing formulas affects homepage/discover | | View/download counters | product metrics, medals, trending inputs | | Upload/derivative pipeline | data loss / hash-addressed CDN | | Meilisearch index settings | search relevance; Scout observer disabled on Artwork by design | | Billing / Cashier / Academy | money | | Maturity / moderation | legal/safety | | Conditional sessions | auth bugs if skip logic wrong | | Sitemap publish | SEO indexing | | ControlPanel `/cp` | admin | | Vector/CLIP/YOLO gateways | latency and cost | | Legacy routes + oldSite | accidental dual-write / dead links | --- ## 40. Production Data Required Collect values only — **never passwords, tokens, keys**. - OS, CPU, RAM, disk type/latency - nginx version + **full vhost** (gzip/brotli, HTTP/2/3, body size, includes of deploy snippets) - PHP-FPM version, pool (`pm`, max_children), `memory_limit`, `max_execution_time`, `upload_max_filesize`, `post_max_size`, OPcache - MySQL version, buffer pool, connections, slow_query_log threshold, **table sizes/row estimates** - Redis version, memory, eviction, Horizon vs supervisor actually running, queue depths, failed jobs - Meilisearch host, index sizes, RAM - CDN provider, cache hit ratio, bandwidth - Production `.env` **non-secret** drivers: `CACHE_STORE`, `SESSION_DRIVER`, `QUEUE_CONNECTION`, `SCOUT_DRIVER`, `FILESYSTEM_DISK`, `UPLOAD_QUEUE_DERIVATIVES`, `DOWNLOAD_ACCEL_*`, `SITEMAPS_*`, `APP_DEBUG` - Request QPS, peak, top URLs, 404 rate - Whether `composer install --no-dev` is used - Whether debugbar/horizon are publicly reachable - Confirm skinbase.org vs skinbase.top relationship --- ## 41. Recommended Production SQL Diagnostics Read-only. Prefer `information_schema` estimates over `COUNT(*)` on huge tables. ```sql -- Database size SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024,1) AS mb FROM information_schema.tables WHERE table_schema = DATABASE(); -- Table sizes (no full counts) SELECT table_name, table_rows, ROUND(data_length/1024/1024,1) AS data_mb, ROUND(index_length/1024/1024,1) AS index_mb FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY data_length+index_length DESC LIMIT 40; SHOW INDEX FROM artworks; SHOW INDEX FROM artwork_metric_snapshots_hourly; SHOW INDEX FROM rank_artwork_scores; SHOW INDEX FROM tags; SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Slow_queries'; SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; ``` EXPLAIN candidates (use production-realistic binds, avoid long locks): - Current `nova:recalculate-rankings` / trending SQL (compare to old Q1/Q2) - Artwork related `orWhereHas` pattern - News LIKE search - `LOWER(username)` profile lookup - `artwork_view_events` insert rate vs table size Do **not** `SELECT *` dump `artwork_metric_snapshots_hourly`. --- ## 42. Recommended Server Diagnostics Read-only: ```text uptime; free -h; df -h iostat -x 1 5 ps aux | egrep 'php-fpm|nginx|redis|meili|horizon|queue:work|ssr.js' nginx -T | egrep 'gzip|brotli|http2|client_max_body|limit_req|include' php -i | egrep 'opcache|memory_limit|max_execution|upload_max|post_max' redis-cli INFO memory php artisan horizon:status php artisan queue:failed --json | head ``` --- ## 43. Benchmark URL Set Use templates; do not invent IDs. 1. `GET /` (anonymous, cold and warm) 2. `GET /` (authenticated) 3. `GET /discover/trending` 4. `GET /discover/fresh` 5. `GET /explore` 6. `GET /explore/{type}/trending` (e.g. wallpapers) 7. `GET /{contentType}/{category}` page 1 and page 5 8. `GET /art/{id}/{slug}` typical 9. `GET /art/{id}/{slug}` high-comment artwork 10. `GET /@{username}` 11. `GET /search?q={common-term}` 12. `GET /search` (empty) 13. `GET /tags/{popular-tag}` 14. `GET /download/artwork/{id}` (auth and guest) 15. `POST /api/art/{id}/view` 16. `GET /sitemap.xml` 17. `GET /sitemaps/artworks-0001.xml` (or first shard) 18. `GET /news/{slug}` 19. `GET /categories` 20. Unknown URL 404 (`/no-such-page-xyz`) --- ## 44. Unknowns / Limitations - Production `.env` and live nginx/PHP/MySQL not inspected. - Dirty working tree means some findings (sitemaps, robots, search, academy) include **uncommitted** work. - Full `php artisan test` log truncated; not a complete pass/fail census. - `npm audit` / production build not re-run. - ControlPanel internals not query-audited in depth. - Subagent fan-out expired; this report is from direct inspection. - Lighthouse file is skinbase.top, March 2026 — may not match skinbase.org today. - Slow-query document predates or coincides with batch1 indexes; residual UNKNOWN. --- ## 45. Repository Changes Made ```text Created: - docs/skinbase-optimization-audit.md Modified: - none (by this audit) Application source changes: - none Database changes: - none Configuration changes: - none ``` Pre-existing dirty files were left untouched. Verify after write with `git status --short` / `git diff --stat` (audit file should be the only new path from this milestone).