> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theaitracker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture overview

> How requests, queued jobs and scoring fit together.

The application is a single Laravel deployment with three fairly distinct halves: an
Inertia-rendered dashboard, a queue-driven data collection layer that talks to external
providers, and a scoring layer that turns collected raw answers into the numbers shown in
the UI.

## Request layer

Every dashboard route sits behind `['auth', 'verified', 'onboarded']`. The `onboarded`
middleware is the important one — it forces any user with an incomplete profile back to
`/setup`, so onboarding is mandatory rather than skippable.

Pages are scoped to an **active campaign** held in the session under `selected_campaign_id`.
The sidebar switcher (`POST /campaigns/select`) validates that the campaign belongs to the
user via `accessibleCampaigns()`, sets the session key, and always redirects to
`/visibility` — never back to the current page, since a page like a prompt detail view
belongs to the campaign the user just switched away from.

<Note>
  `Campaign::getRouteKeyName()` returns `uuid`, so campaigns are addressed by UUID in URLs,
  never by autoincrement ID.
</Note>

## Service layer

`app/Services` holds one class per external system plus the domain logic:

<AccordionGroup>
  <Accordion title="LLM providers">
    `Services/LLM` has a provider class per model family: `OpenAiProvider`,
    `GeminiProvider`, `ClaudeProvider`, `PerplexityProvider`, `GrokProvider`. Google's AI
    Overview and AI Mode surfaces are reached through SerpApi rather than a direct API.
  </Accordion>

  <Accordion title="Brand sentiment">
    `Services/BrandSentiment` runs as a small pipeline: `TopicSelector` →
    `TopicDataRetriever` (using `RetrievalPromptWriter`) → `TopicSentimentScorer` (using
    `EvaluationPromptWriter`) → `BrandSentimentFinalizer`.
  </Accordion>

  <Accordion title="Onboarding generators">
    `Services/Onboarding` contains the wizard's generators — `BrandNameGenerator`,
    `BusinessDescriptionGenerator`, `BrandVariantsGenerator`, `TopicGenerator`,
    `PromptRetrievalGenerator` — plus `MasterTaxonomy` and `LocaleMapper`.
  </Accordion>

  <Accordion title="Analytics and site data">
    `GoogleAnalyticsService`, `Google/SearchConsoleService`, `BingWebmasterService`,
    `PageSpeedService`, `SiteInfoService`, `Semrush` and
    `GoogleSearchScreenshotService`.
  </Accordion>

  <Accordion title="Scoring and competitors">
    `IndustryVisibilityScorer`, `VisibilityDetector`, `CompetitorDiscovery` and
    `CompetitorClassifier`.
  </Accordion>
</AccordionGroup>

## Collection layer

No provider call happens in the request cycle. Controllers dispatch jobs and the UI polls
for status — for example `GET /api/sentiment/status` backs the sentiment page's polling.

Jobs come in `Fetch*` / `Process*` pairs: `FetchOpenAiSentimentJob` collects, then
`ProcessOpenAiAnswersJob` parses answers and records detections. `TokenUsageTracker` and the
`LlmUsage` model record spend per call, and `PromptRunFailureAlert` surfaces batches that
failed.

Failures are also reported to Mattermost through a custom log channel when
`MATTERMOST_LOG_ENABLED=true`.

## Scoring layer

`config/scoring.php` defines the weighted formula:

```
score = Σ ( llm_weight × 100 × (count / max)^exponent )
```

Provider weights must sum to `1.0` and default to OpenAI `0.30`, Perplexity `0.25`,
Gemini `0.20`, Grok `0.10`, AI Overview `0.10` and AI Mode `0.05`. A
`rank_degradation_exponent` of `1.2` penalises lower mention counts more steeply than a
linear scale, `min_normalization` prevents dividing by very small maxima, and
`non_competitor_penalty` halves the contribution of domains the classifier did not mark as
direct competitors. Every weight is overridable via `SCORING_*` environment variables.

## Frontend

React 19 + Inertia 2 with TypeScript. Controllers return props; there is no separate REST
API for the dashboard, though several JSON endpoints exist for polling and report data
(`/campaigns/{campaign}/traffic/summary`, the `reports/*` API routes, and so on). Ziggy
exposes named routes to TypeScript.
