<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>

<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>

## About Laravel

Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:

- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).

Laravel is accessible, powerful, and provides tools required for large, robust applications.

## Learning Laravel

Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.

If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.

## Laravel Sponsors

We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).

### Premium Partners

- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**

## Contributing

Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).

## Code of Conduct

In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).

## Security Vulnerabilities

If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.

## Vision & AI Auto-Tagging Integration

## Upload UI Feature Flag (`uploads.v2`)

The new React upload wizard is behind a feature flag and is **disabled by default**.

- Flag env var: `SKINBASE_UPLOADS_V2`
- Config key: `features.uploads_v2`
- Client flags source: `window.SKINBASE_FLAGS`

### Default behavior

- `SKINBASE_UPLOADS_V2=false` → legacy upload UI is rendered.
- `SKINBASE_UPLOADS_V2=true` → `UploadWizard` is rendered.

### Setup

In `.env` (or `.env.example` for project defaults):

```dotenv
SKINBASE_UPLOADS_V2=false
```

Enable explicitly when ready:

```dotenv
SKINBASE_UPLOADS_V2=true
```

After changing env values, clear/reload config as usual:

```bash
php artisan config:clear
```

The system intentionally keeps legacy upload as the default until the flag is explicitly turned on.

## Upload Moderation UI Flow

Admin moderation for draft uploads is available through a dedicated queue page.

- Page route: `/admin/uploads/moderation`
- Access: authenticated users with `role=admin` or `role=moderator`
- Data source: `GET /api/admin/uploads/pending`

### Queue behavior

1. The page loads pending draft uploads (`moderation_status=pending`).
2. Moderators can enter an optional note per upload.
3. Approve action calls:

  - `POST /api/admin/uploads/{id}/approve`
  - Sets moderation to approved and records moderator + timestamp.

4. Reject action calls:

  - `POST /api/admin/uploads/{id}/reject`
  - Sets upload status/processing state to rejected and stores note.

### Publish gate

- Normal users can publish only when `moderation_status=approved`.
- Admin users can publish with override behavior.

## Similar Artworks Analytics (A/B Evaluation)

The artwork page similar-items block emits two event types:

- `impression` (block rendered)
- `click` (item clicked)

Events are stored in `similar_artwork_events` and aggregated daily into `similar_artwork_daily_metrics` by `algo_version`.

- Ingest endpoint: `POST /api/analytics/similar-artworks`
- Aggregation command: `php artisan analytics:aggregate-similar-artworks --date=YYYY-MM-DD`
- Scheduler: runs daily at `03:10`

## Personalized Discovery Foundation (Phase 8)

This foundation adds versioned, async-only ingestion and profile normalization for personalized discovery.

- Tables:
  - `user_interest_profiles`
  - `user_discovery_events`
  - `user_recommendation_cache`
- Ingest endpoint: `POST /api/discovery/events` (auth required)
- Supported event types: `view`, `click`, `favorite`, `download`
- Processing model: non-blocking queue job (`IngestUserDiscoveryEventJob`)
- Normalization: recency-decay + score normalization in `UserInterestProfileService`

No feed ranking/UI behavior is introduced in this foundation step.

### Feed Endpoint Skeleton

The backend now exposes a personalized feed API skeleton:

- Endpoint: `GET /api/v1/feed` (auth required)
- Query params:
  - `limit` (1-50, default 24)
  - `cursor` (opaque cursor token for pagination)
  - `algo_version` (optional override)
- Response includes `data` items and `meta.next_cursor` for cursor pagination.

Behavior:

- Reads `user_recommendation_cache` by `user_id + algo_version`.
- On cache miss/stale, returns immediate fallback results and dispatches async regeneration job.
- Regeneration runs in queue (`RegenerateUserRecommendationCacheJob`) and writes refreshed cache.
- Includes cold-start fallback (`popular + similar`) and a diversity guard to avoid near-duplicates.

## Feed Analytics Instrumentation

Feed analytics now track:

- `feed_impression`
- `feed_click`

Payload dimensions:

- `user_id` (derived from auth session)
- `artwork_id`
- `position`
- `algo_version`
- `source` (`personalized`, `cold_start`, `fallback`)

Optional:

- `dwell_seconds` (for click dwell bucket metrics)

Endpoints:

- Ingest: `POST /api/analytics/feed` (auth required)
- Daily aggregation: `php artisan analytics:aggregate-feed --date=YYYY-MM-DD`
- Admin report: `GET /api/admin/reports/feed-performance`

Daily metrics include CTR, save-rate, and dwell buckets.

For non-blocking client transport, use `navigator.sendBeacon` with `fetch(..., { keepalive: true })` fallback.
Reference helper: `resources/js/lib/feedAnalytics.js`.

## Phase 8B: Ranking Weight Tuning (Manual + Data-Driven)

Discovery ranking now supports versioned blend weights per `algo_version` in `config/discovery.php`.

- Blend terms: `w1` interest, `w2` recency, `w3` popularity, `w4` novelty
- Per-algo sets: `discovery.ranking.algo_weight_sets`
- Safe rollout: deterministic traffic split by `algo_version` with config gates (`g10`, `g50`, `g100`)
- Emergency rollback: `DISCOVERY_FORCE_ALGO_VERSION=clip-cosine-v1`

Offline evaluator and A/B helper:

- Evaluate objective across one/all algos:
  - `php artisan analytics:evaluate-feed-weights --from=YYYY-MM-DD --to=YYYY-MM-DD`
  - Optional: `--algo=clip-cosine-v1`
- Baseline vs candidate comparison:
  - `php artisan analytics:compare-feed-ab clip-cosine-v1 clip-cosine-v2 --from=YYYY-MM-DD --to=YYYY-MM-DD`

Objective score uses `feed_daily_metrics` and configurable objective weights in `discovery.evaluation.objective_weights`.

Temporary production policy: set `DISCOVERY_EVAL_SAVE_RATE_INFORMATIONAL=true` to keep `save_rate` visible but excluded from objective score until save-event ingestion is verified.

Operational runbook: `docs/feed-rollout-runbook.md`.

## Operations / Runbooks

- Upload UI v2 rollout, post-deploy monitoring, and rollback: `docs/ui/upload-v2-rollout-runbook.md`
- Feed rollout and rollback: `docs/feed-rollout-runbook.md`
- Registration anti-spam and email quota protection: `docs/registration-antispam.md`

No automatic tuning is enabled in this phase.

Skinbase uses asynchronous AI tagging via `AutoTagArtworkJob`.
The job calls external vision services (CLIP and optional YOLO), normalizes tags, and attaches them through `TagService` as AI tags with confidence values.

### Critical Safety Rule

⚠️ **Publish must never depend on vision services.**

- Upload/publish flow dispatches AI tagging to queue after publish work.
- Vision failures, timeouts, or service outages must not block artwork publish.
- If AI tagging fails, artwork remains published and can be tagged later (retry/manual/batch).

### Environment Variables (Vision)

Set these in `.env` (all are optional; defaults are in `config/vision.php`):

#### Global

- `VISION_ENABLED` (default: `true`)
  - Master switch for all AI auto-tagging.
- `VISION_QUEUE` (default: `default`)
  - Queue name used by `AutoTagArtworkJob`.
- `VISION_IMAGE_VARIANT` (default: `md`)
  - Derivative variant sent to vision services (e.g. `md`, `lg`).

#### CLIP

- `CLIP_BASE_URL` (default: empty)
  - Base URL for CLIP service (example: `https://clip.internal`).
  - If empty, CLIP call is skipped.
- `CLIP_ANALYZE_ENDPOINT` (default: `/analyze`)
  - Path appended to `CLIP_BASE_URL`.
- `CLIP_TIMEOUT_SECONDS` (default: `8`)
  - Request timeout for CLIP calls.
- `CLIP_CONNECT_TIMEOUT_SECONDS` (default: `2`)
  - Connection timeout for CLIP calls.
- `CLIP_HTTP_RETRIES` (default: `1`)
  - HTTP retry attempts for CLIP requests.
- `CLIP_HTTP_RETRY_DELAY_MS` (default: `200`)
  - Delay between CLIP retries.

#### YOLO (optional)

- `YOLO_ENABLED` (default: `true`)
  - Enables YOLO integration.
- `YOLO_BASE_URL` (default: empty)
  - Base URL for YOLO service. If empty, YOLO call is skipped.
- `YOLO_ANALYZE_ENDPOINT` (default: `/analyze`)
  - Path appended to `YOLO_BASE_URL`.
- `YOLO_TIMEOUT_SECONDS` (default: `8`)
  - Request timeout for YOLO calls.
- `YOLO_CONNECT_TIMEOUT_SECONDS` (default: `2`)
  - Connection timeout for YOLO calls.
- `YOLO_HTTP_RETRIES` (default: `1`)
  - HTTP retry attempts for YOLO requests.
- `YOLO_HTTP_RETRY_DELAY_MS` (default: `200`)
  - Delay between YOLO retries.
- `YOLO_PHOTOGRAPHY_ONLY` (default: `true`)
  - When `true`, YOLO is called only for artworks in photography content type.

### Expected CLIP Response Format

CLIP `/analyze` should return tags as either a direct list or under `tags` / `data`:

```json
[
  { "tag": "cyberpunk", "confidence": 0.42 },
  { "tag": "city", "confidence": 0.31 }
]
```

Also accepted:

```json
{
  "tags": [
    { "tag": "cyberpunk", "confidence": 0.42 }
  ]
}
```

or

```json
{
  "data": [
    { "tag": "cyberpunk", "confidence": 0.42 }
  ]
}
```

### Expected YOLO Response Format

YOLO may return the same tag list format as CLIP, or object detections:

```json
{
  "objects": [
    { "label": "person", "confidence": 0.91 },
    { "label": "camera", "confidence": 0.67 }
  ]
}
```

`label` values are converted to tags, confidence is preserved when present.

### AutoTagArtworkJob Behavior

- Calls CLIP `/analyze` when `VISION_ENABLED=true` and `CLIP_BASE_URL` is set.
- Optionally calls YOLO based on `YOLO_ENABLED` and `YOLO_PHOTOGRAPHY_ONLY`.
- Merges CLIP + YOLO tags and keeps highest confidence for duplicates.
- Normalizes tags before attach (lowercase, cleanup, slug-safe format).
- Uses `TagService::attachAiTags()` to store pivot data:
  - `source = ai`
  - `confidence = <float|null>`
- Runs with queue retry + timeout safety (`tries`, `backoff`, `timeout`).
- Logs failures with reference/context for troubleshooting.
- On non-retriable response scenarios (e.g. 4xx), job exits safely without blocking publish.

### Queue / Worker Requirements (`VISION_QUEUE`)

- Ensure a worker is running for the configured queue.
- Example worker command:

```bash
php artisan queue:work --queue=default
```

- Scout/Meilisearch syncing uses the `search` queue, so artwork indexing workers must include that queue as well:

```bash
php artisan queue:work --queue=search,default
```

- If `VISION_QUEUE=vision`, run worker for that queue:

```bash
php artisan queue:work --queue=vision
```

- In production, use Supervisor/systemd/Horizon to keep workers alive.
- Without an active worker, auto-tagging jobs remain queued and will not execute.

### Local vs Production Notes

#### Local development

- For fully offline local work, set `VISION_ENABLED=false`.
- Or set only `CLIP_BASE_URL`/`YOLO_BASE_URL` you can reach locally.
- Prefer short timeouts to avoid slow dev feedback loops.

#### Production

- Use internal/private service endpoints for CLIP/YOLO when possible.
- Keep conservative timeouts and low retry counts to prevent queue congestion.
- Monitor failed jobs and logs for vision service reliability.
- Scale queue workers based on upload volume and service latency.

### Verify Setup (Health + Test Call)

After configuring env vars and restarting workers, verify in this order:

Quick helper (PowerShell):

```powershell
pwsh -File ./scripts/vision-smoke.ps1
```

Optional flags:

```powershell
pwsh -File ./scripts/vision-smoke.ps1 -EnvFile ".env" -SampleImageUrl "https://files.skinbase.org/img/aa/bb/cc/md.webp"
pwsh -File ./scripts/vision-smoke.ps1 -SkipAnalyze
```

1. Confirm queue worker is consuming `VISION_QUEUE`.

```bash
php artisan queue:work --queue=default
```

1. Check CLIP/YOLO health endpoints (replace host/port as needed):

```bash
curl -fsS "$CLIP_BASE_URL/health"
curl -fsS "$YOLO_BASE_URL/health"
```

1. Make a direct analyze test call (CLIP example):

```bash
curl -X POST "$CLIP_BASE_URL$CLIP_ANALYZE_ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"image_url":"https://files.skinbase.org/img/aa/bb/cc/md.webp"}'
```

1. Trigger an upload/publish and confirm:

- Publish response succeeds even if CLIP/YOLO is down.
- `AutoTagArtworkJob` is queued/executed asynchronously.
- AI tags appear on the artwork when services are healthy.
- Failures are logged, but publish is unaffected.

## Queue workers

The contact form mails are queued. To process them you need a worker. Locally you can run a foreground worker:

```
php artisan queue:work --sleep=3 --tries=3
```

For production we provide example configs under `deploy/`:

- `deploy/supervisor/skinbase-queue.conf` — Supervisor config
- `deploy/systemd/skinbase-queue.service` — systemd unit file

See `docs/QUEUE.md` for full setup steps and commands.

## License

The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
