Search by

padosoft / product-image-discovery-admin

lopadova

Laravel admin demo application for padosoft/product-image-discovery.

Package info

github.com/padosoft/product_image_discovery_admin

Language:JavaScript

Type:project

pkg:composer/padosoft/product-image-discovery-admin

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 4

v1.1.0 2026-05-04 20:12 UTC

README

CI Release PHP Laravel React Vite License

Professional Laravel admin console for padosoft/product-image-discovery: inspect discovery requests, approve or reject candidates, tune provider configuration, run diagnostics, and ship safer product-image workflows without building a back office from scratch.

Product Image Discovery Admin overview

Table of Contents

Why Teams Use It

Product images are operational data. A wrong product-color image can leak into ERP, PIM, marketplaces, catalogs, feeds, and customer-facing storefronts. This admin app gives catalog and operations teams a dense, production-shaped cockpit for the conservative discovery pipeline in padosoft/product-image-discovery.

  • Review uncertain matches before they publish.
  • Compare candidates with source, score, AI, quality, and audit evidence.
  • Manage thresholds, providers, trusted sources, and client overrides.
  • Run debug flows against real or fake providers.
  • Inspect health, queues, storage, AI configuration, and redacted reports.
  • Export filtered request data and keep demo operator filters locally.

What You Get

  • Operations dashboard with KPI cards, score distribution, provider readiness, queue phase health, latest attention items, and shortcuts.
  • Requests and Manual Review pages with dense filters, pagination, CSV export, detail drawer, compare mode, protected image previews, approve/reject/retry flows, and audit timeline.
  • Configuration tools for settings, search providers, provider credential status, trusted sources, trust scores, policies, scopes, and URL patterns.
  • Diagnostics suite with guided debug-flow runner, report viewer, JSON search, redacted report download, health checks, and API workbench.
  • Offline demo path with deterministic seeded data for Herno, Nike, and New Balance scenarios.
  • Release gate through PHPUnit, Vitest, Vite, Playwright, Composer validation, and GitHub Actions.

Screenshots

Overview Dashboard

Overview dashboard

Request Search

Request search

Request Detail And Candidate Review

Request detail

API Test Workbench

API test workbench

Debug Flow

Debug flow setup

Debug Flow Results

Debug flow results

Scoring Configuration

Scoring configuration

Trusted Sources Configuration

Trusted sources configuration

Architecture

This repository is a Laravel 13 admin/demo application, not a reusable UI package. It consumes the sibling headless package through a Composer path repository:

..\product_image_discovery
..\product_image_discovery_admin

The stack is intentionally boring and inspectable:

  • Laravel 13 host app.
  • SQLite by default for local development and tests.
  • Blade shell at the admin route.
  • Vite + React for the operator UI.
  • Session/CSRF-protected admin JSON wrappers.
  • Package API left available under its own prefix.
  • Playwright browser smoke coverage for desktop and tablet layouts.

Quickstart

End-to-end walkthrough from a fresh clone to a working Brave-backed debug run on macOS/Linux. Windows/Herd users have an equivalent block below in Local Setup.

Prerequisites: PHP 8.3+, Composer, Node 20+ (npm 10+), and a checkout of the sibling package next to this repo:

parent/
├─ product_image_discovery/         # headless package
└─ product_image_discovery_admin/   # this repo

1. Install dependencies

composer install
npm ci

2. Configure the environment

cp .env.example .env
php artisan key:generate
touch database/database.sqlite

Open .env and confirm at minimum:

APP_URL=http://127.0.0.1:8000
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database/database.sqlite   # or leave default
SESSION_DRIVER=file
PID_ADMIN_ROUTE_MIDDLEWARE=web,auth
PID_ADMIN_DEBUG_RUN_MIDDLEWARE=auth

BRAVE_SEARCH_API_KEY is intentionally not read from .env. Search-provider credentials live in the database (write-only via the admin UI). See step 6.

3. Migrate and seed demo data

php artisan migrate
php artisan pid-admin:seed-demo

This creates:

  • the deterministic Herno / Nike / New Balance demo requests,
  • a demo fake-demo search provider (seeded is_active=true, priority=1),
  • a demo admin user — email admin@demo.test, password password, only when APP_ENV is local or testing. In any other environment the seeder skips the user step so a known-credentials operator never lands in a non-dev DB. Use pid-admin:create-user for staging/production.

The artisan command is namespaced. Use pid-admin:seed-demo, not seed-demo.

If you want a real user instead of the demo one:

php artisan pid-admin:create-user you@example.com --name="You" --password=secret
# omit --password to receive a generated one (printed once)

API credentials for an external client (e.g. Gescat) are created with three commands; each secret is printed once:

# 1. technical user that only owns API tokens (skipped if it already exists)
php artisan pid-admin:create-api-user gescat-api@surfacesrl.com --name="Gescat API"
# 2. non-expiring Sanctum token with product-image-discovery:read + :write
php artisan pid-admin:create-api-token gescat-api@surfacesrl.com --name=gescat
# 3. Price Intelligence tenant (created or reused) + pi_ API key without extra scopes
php artisan pid-admin:create-price-key gescat --tenant-name=Gescat

4. Build the frontend and start the server

npm run build
php artisan serve

Leave php artisan serve running. For frontend hot reload during development run npm run dev in a second terminal instead of npm run build.

5. Sign in

Open http://127.0.0.1:8000/login, log in with admin@demo.test / password, and you'll land on http://127.0.0.1:8000/admin/product-image-discovery.

The whole admin shell (Blade + React) and its JSON endpoints sit behind web,auth by default (PID_ADMIN_ROUTE_MIDDLEWARE), so guests are redirected to /login. The Logout button lives in the topbar.

6. Configure a real search provider (Brave)

The demo fake-demo provider is intentionally first in priority so out-of-the-box runs work offline. To exercise a real Brave search you need to either disable the fake provider or give Brave a higher priority (lower number).

  1. Sign in, then open http://127.0.0.1:8000/admin/product-image-discovery/providers.
  2. Edit fake-demo and set is_active = false, or keep it active and bump its priority to a high number (e.g. 100).
  3. Create or update a Brave provider:
    • code: brave
    • driver: brave
    • name: Brave
    • base_url: https://api.search.brave.com
    • api_key: your Brave API key (write-only — saved encrypted, never echoed back)
    • is_active: true
    • priority: 1 (or any value lower than the rest)

Provider selection: SearchProviderManager orders active providers by priority ASC and uses the first that returns non-empty results. The rest are treated as fallbacks.

7. Run a debug flow against Brave

  1. Open http://127.0.0.1:8000/admin/product-image-discovery/debug.

  2. Leave the default Herno payload (or paste your own product JSON). The fields the package actually reads are:

    Required Optional
    client_id, erp_model_color_id, brand supplier, supplier_sku, model_code, color_code, color_name, category, description, ean, season, material, metadata (any keys)

    Anything outside this set is preserved on the request but not used by the search/scoring/AI pipeline. description (or its fallbacks name / title / metadata.description / metadata.title) is used as a search term when model_code is empty, so for products that don't have a clean SKU it's important to fill it in.

  3. Recommended options for a first real test:

    • no_env_brave: true (default) — keep it; we are using the DB provider, not auto-creating one from env.
    • no_download: true (default) — skip downloading the candidate images. You will still see them via the View Image column in the report.
    • max_candidates: 2 (default) or higher.
  4. Click Run debug. The right panel polls until status becomes succeeded or failed.

8. Inspect the report

Open http://127.0.0.1:8000/admin/product-image-discovery/reports, click Open on the latest run.

The Request tab shows two sections:

  • package_request_summary — the slim summary the package emits in the report (id, status, brand, model_code, color_name, scores, candidate pointers).
  • original_request_payload — the full JSON you submitted to the debug flow, redacted. This is the place to verify that description, ean, metadata, and any other optional field actually arrived at the package.

In the Candidates tab the table is sorted by final_score desc — the top row is the best match. Each row exposes:

  • View URL — opens source_page_url in a new tab (the page Brave found)
  • View Image — opens image_url in a new tab (works even with no_download: true)

Empty/invalid URLs render as a disabled chip.

Common pitfalls

  • 401 Unauthorized on /debug-runs — you are not signed in. Visit /login (step 5).
  • Command "seed-demo" is not defined — use the prefix: php artisan pid-admin:seed-demo.
  • BRAVE_SEARCH_API_KEY missing on the Health screen — the slot label is now BRAVE_SEARCH_PROVIDER. It checks the DB, not env. Configure a Brave provider (step 6).
  • Brave never gets called — the fake-demo provider has priority: 1 and short-circuits the chain. Disable it or raise its priority (step 6).
  • All candidates land in manual_review even with high score — that's expected without trusted sources. Add the candidate's host on /admin/product-image-discovery/trusted if you want verified_match/ready_to_publish.
  • You changed React/CSS and don't see the change — npm run build writes to public/build/; the browser may cache the old bundle. Hard refresh (Cmd+Shift+R on macOS, Ctrl+F5 on Linux). For iterative work prefer npm run dev.

Local Setup

Keep the headless package checked out beside this app:

..\product_image_discovery
..\product_image_discovery_admin

Install and bootstrap the app:

composer install
Copy-Item .env.example .env -ErrorAction SilentlyContinue
php artisan key:generate --ansi
New-Item -ItemType File database\database.sqlite -Force
php artisan migrate
php artisan pid-admin:seed-demo --fresh
npm ci
npm run build
php artisan serve

Open:

http://127.0.0.1:8000/admin/product-image-discovery

If php is not on PATH, this Windows/Herd workstation uses:

%USERPROFILE%\.config\herd\bin\php84\php.exe

Login

Debug-run endpoints (POST /admin/product-image-discovery/debug-runs, POST /admin/product-image-discovery/requests) sit behind the auth middleware controlled by pid-admin.debug_run_middleware. The rest of the admin shell is also behind auth through PID_ADMIN_ROUTE_MIDDLEWARE=web,auth.

The app ships with three minimal endpoints, registered outside the admin prefix:

GET  /login
POST /login
POST /logout

The pid-admin:seed-demo command seeds a demo operator suitable for local/offline testing only when APP_ENV is local or testing:

  • email: admin@demo.test
  • password: password

Outside those environments the seeder skips the demo user, so a well-known credential never lands in a staging or production database. Use pid-admin:create-user instead.

Create or update a real user with the artisan helper:

php artisan pid-admin:create-user me@example.com --name="Op"
# omit --password to receive a generated one (printed once)
php artisan pid-admin:create-user me@example.com --password=secret

The admin topbar exposes a Logout button that submits a CSRF-protected POST /logout.

To bypass auth in CI or short-lived local smoke tests, set PID_ADMIN_ROUTE_MIDDLEWARE=web and leave PID_ADMIN_DEBUG_RUN_MIDDLEWARE empty (Playwright does this). PHPUnit keeps web,auth and acts as a signed-in user by default via Tests\TestCase::$authenticateByDefault.

Demo Data

The local seeder gives you deterministic, offline-friendly scenarios:

  • Herno manual-review fashion request.
  • Nike candidate review and retry/no-candidate workflows.
  • New Balance ready/selected-image example.
  • Fake demo provider and trusted demo sources.
  • Inline generated candidate images so previews work without live downloads.

Reset the demo state with:

php artisan pid-admin:seed-demo --fresh

Testing

Run the full local release gate:

composer validate --strict
npm run phpunit
npm run test
npm run build
npm run e2e

npm run phpunit is the preferred PHP test entry point on this machine because it resolves Herd PHP 8.4 before falling back to php on PATH.

CI

GitHub Actions runs the same release gate on pushes and pull requests:

  • checkout this admin app and the sibling padosoft/product_image_discovery package
  • composer validate --strict
  • composer install
  • npm ci
  • npm run phpunit
  • npm run test
  • npm run build
  • npm run e2e:ci
  • upload Playwright traces/screenshots on failure

Queues

Pipeline jobs from padosoft/product-image-discovery and padosoft/laravel-ai-price-intelligence are dispatched on dedicated queues, not on default. A worker that only listens to default never runs them: requests stay queued.

Queue names come from config and can be changed from the environment without touching code:

Env var Effect
PRODUCT_IMAGE_DISCOVERY_QUEUE routes every image-discovery stage to one queue
PRODUCT_IMAGE_DISCOVERY_QUEUE_<STAGE> overrides one stage (INGEST, SEARCH, EXTRACT, VERIFY, DOWNLOAD, QUALITY)
PRICE_INTELLIGENCE_QUEUE routes every price-intelligence lane to one queue
PRICE_INTELLIGENCE_QUEUE_<LANE> overrides one lane (DISCOVERY, SCRAPE, ENRICH, AI, NOTIFICATIONS)

Without env vars the defaults are the historical names (image-discovery-<stage>, pi-<lane>). FETCH, ENHANCE and DESCRIPTION exist in the map but the package has no job for them yet.

What each image-discovery stage actually does (package v1.1):

Stage Work Profile
ingest DB upsert, dispatches search light
search external search provider, up to 8 sequential queries external API, rate-limited
extract turns search results into candidates (DB only, no HTML fetch) light
verify one job per candidate; calls the AI vision model when PRODUCT_IMAGE_DISCOVERY_AI_ENABLED=true (timeout 45s), otherwise pure scoring external API when AI is on, light otherwise
download Http::get()->body() of the image, stored on PRODUCT_IMAGE_DISCOVERY_STORAGE_DISK light (a few MB in memory)
quality getimagesize() header read, no pixel decoding light

Recommended setup on Laravel Cloud managed queues. Each managed queue serves exactly one queue name, so route the stages to the names you actually created:

# one managed queue (cheapest): everything on it. Use the exact name shown in Cloud.
PRODUCT_IMAGE_DISCOVERY_QUEUE=default
PRICE_INTELLIGENCE_QUEUE=default

# optional second managed queue, only when search/AI hit rate limits
PRODUCT_IMAGE_DISCOVERY_QUEUE_SEARCH=image-discovery-api
PRODUCT_IMAGE_DISCOVERY_QUEUE_VERIFY=image-discovery-api
  • image-discovery-api (if created): small workers with a low max worker count; the cap keeps search/AI calls under provider rate limits. With AI disabled, verify belongs on the main queue.
  • Managed queues take no --timeout flag. Flex workers cap a job at 90 seconds: keep PRODUCT_IMAGE_DISCOVERY_AI_TIMEOUT well below it (default 45).
  • No large-memory workers are needed: no stage decodes image pixels.
  • Downloaded files are optional. Nothing reads them back (quality uses search metadata, the API exposes the remote image_url, the admin preview falls back to it), so PRODUCT_IMAGE_DISCOVERY_STORAGE_DISK=local works even on ephemeral worker disks. Use object storage only if the files must be kept.

Production On Laravel Cloud

Checklist for a working pipeline (image discovery + price intelligence):

  1. Database: MySQL or Postgres attached to the environment (SQLite is not supported in production).
  2. Deploy command: php artisan migrate --force (the packages auto-load their migrations).
  3. Queues: at least one managed queue plus the *_QUEUE env vars above. aws/aws-sdk-php is required in composer.json for managed queues.
  4. Scheduler: enable it on the App cluster. Price intelligence runs piprice:run-due every minute and piprice:aggregates:daily at 02:30.
  5. Cache: a shared store (CACHE_STORE=database, or an attached Valkey/Redis). The scheduler uses cache locks (withoutOverlapping), which do not work across instances with file.
  6. APP_KEY: set once and never rotated casually: search-provider API keys are stored encrypted with it.
  7. Search providers: run php artisan db:seed --force once to create the default provider rows (all inactive). Never put it in the deploy commands: re-running it resets every default provider to is_active=false and silently disables search. Activate at least one image-capable driver (brave, tavily, exa, firecrawl, searchapi) with its API key from the admin Settings page. With no active provider every request ends in no_candidates_found. Never run pid-admin:seed-demo in production.
  8. AI (optional): PRODUCT_IMAGE_DISCOVERY_AI_ENABLED=true, PRODUCT_IMAGE_DISCOVERY_AI_PROVIDER (regolo, anthropic, openai, openrouter), the matching key (REGOLO_API_KEY, ANTHROPIC_API_KEY, ...) and PRODUCT_IMAGE_DISCOVERY_AI_VISION_MODEL. Price intelligence uses the LLM only with PI_LLM_DRIVER=laravel-ai plus PI_LLM_PROVIDER/PI_LLM_MODEL.
  9. Client credentials: pid-admin:create-api-user, pid-admin:create-api-token, pid-admin:create-price-key (see Login). Image discovery uses Authorization: Bearer <token>; price intelligence uses X-Api-Key: pi_... under /api/v1.
  10. Outbound HTTPS from workers to search APIs, image hosts, AI providers and competitor sites.
  11. Price intelligence panel: APP_URL must be the public URL (e.g. https://image-discovery.surfacesrl.com) so the panel's browser calls are recognized as same-domain, and the gescat tenant must exist.

Security Model

  • Provider credentials are write-only.
  • JSON responses expose credential booleans such as has_api_key, never raw secrets or partial previews.
  • Debug reports and stored artifacts are redacted before display/download.
  • Admin wrappers use same-origin fetch and CSRF/session protection.
  • High-impact debug/request creation endpoints support stricter configurable middleware and throttling.
  • Candidate source URLs are normalized before browser navigation.
  • CSV exports neutralize spreadsheet formula prefixes in untrusted text cells.

Admin Routes

The admin console lives at:

/admin/product-image-discovery

Admin JSON wrappers live under:

/admin/product-image-discovery/...

The package API remains available at:

/api/product-image-discovery/...

Price Intelligence Panel

The web panel of padosoft/laravel-ai-price-intelligence-admin is mounted at /admin/price-intelligence and linked from the console sidebar (Modules → Price Intelligence). The link appears only when the package is installed. It is a React SPA that only calls the price-intelligence API under /api/v1: it adds one route and no tables.

  • Install: the package is not on Packagist; composer.json pulls it from GitHub through a vcs repository.
  • Assets: GitHub archives ship without the built SPA, so the build is committed in public/vendor/price-intelligence-admin. After every composer update padosoft/laravel-ai-price-intelligence-admin run npm run build:price-intelligence-admin and commit the result.
  • Login: same session as the console (web, auth, auth.session, forced password change), then the price-intelligence:admin gate, granted to every console user.
  • API access: /api/v1 adds Sanctum's EnsureFrontendRequestsAreStateful, so same-domain browser requests carry the session and the CSRF token. APP_URL's host is always a stateful domain; add others with SANCTUM_STATEFUL_DOMAINS. Machine clients (Gescat) keep using X-Api-Key and stay stateless.
  • Tenant: single tenant. Session users work on the tenant with code PRICE_INTELLIGENCE_ADMIN_TENANT (default gescat, created by pid-admin:create-price-key gescat). If that tenant does not exist the panel API answers 401.
  • Realtime: PRICE_INTELLIGENCE_ADMIN_REALTIME=polling by default (every 15s); sse keeps a PHP worker busy per open page.

Relationship To The Package

Use this app when you want the ready-made professional admin experience. Use padosoft/product-image-discovery directly when you only need the headless API, models, jobs, configuration, and package-level integration surface.

The admin app intentionally stays close to the package contracts and avoids exposing implementation-only secrets or internal raw payloads.

Process Docs

Read AGENTS.md first when resuming this project. The durable implementation plan is saved at:

%USERPROFILE%\Downloads\productimagesearch-admin\plan.md

The project history and non-obvious setup lessons are tracked in:

License

Apache-2.0. See LICENSE.

Auth and forced password change

  • The whole admin (/admin/product-image-discovery/..., plus /admin which redirects to it) requires login via PID_ADMIN_ROUTE_MIDDLEWARE=web,auth. Guests navigating pages are redirected to /login; JSON requests (Accept: application/json) get 401, and the React shell redirects to /login on 401.
  • php artisan pid-admin:create-user {email} always sets users.must_change_password = true, both on create and when re-run with --password on an existing user. There is no opt-out. At the next login the user is redirected to /password/change (min. 8 characters, different from the current one); admin JSON endpoints answer 409 password_change_required until the password is changed.
  • The admin group also runs auth.session: when a user's password changes (from /password/change or a create-user --password reset), every other session and remember-me cookie of that user is logged out; the session that changed it stays signed in.
  • /admin/product-image-discovery/health is an authenticated diagnostics page (environment, debug flag, configured keys, providers). For external uptime monitoring use Laravel's public /up endpoint.
  • The demo user seeded locally (admin@demo.test) is not forced to change password.
  • Run php artisan migrate to add the must_change_password column. Package API routes (/api/product-image-discovery) are not affected.