1131 lines
53 KiB
Markdown
1131 lines
53 KiB
Markdown
# 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).
|