Files
SkinbaseNova/docs/skinbase-optimization-audit.md
T

53 KiB
Raw Blame History

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

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
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)

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 /

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?}

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

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

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

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

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

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)

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

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

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

/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:

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

Read-only. Prefer information_schema estimates over COUNT(*) on huge tables.

-- 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.


Read-only:

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

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).