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.
Campaign::getRouteKeyName() returns uuid, so campaigns are addressed by UUID in URLs,
never by autoincrement ID.Service layer
app/Services holds one class per external system plus the domain logic:
LLM providers
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.Brand sentiment
Brand sentiment
Services/BrandSentiment runs as a small pipeline: TopicSelector →
TopicDataRetriever (using RetrievalPromptWriter) → TopicSentimentScorer (using
EvaluationPromptWriter) → BrandSentimentFinalizer.Onboarding generators
Onboarding generators
Services/Onboarding contains the wizard’s generators — BrandNameGenerator,
BusinessDescriptionGenerator, BrandVariantsGenerator, TopicGenerator,
PromptRetrievalGenerator — plus MasterTaxonomy and LocaleMapper.Analytics and site data
Analytics and site data
GoogleAnalyticsService, Google/SearchConsoleService, BingWebmasterService,
PageSpeedService, SiteInfoService, Semrush and
GoogleSearchScreenshotService.Scoring and competitors
Scoring and competitors
IndustryVisibilityScorer, VisibilityDetector, CompetitorDiscovery and
CompetitorClassifier.Collection layer
No provider call happens in the request cycle. Controllers dispatch jobs and the UI polls for status — for exampleGET /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:
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.