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

1131 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).