['label' => 'Overall status banner', 'help' => 'The "All systems operational" headline.', 'default' => true], 'page.show_uptime' => ['label' => 'Uptime bar and percentage', 'help' => 'The 90 day bar under the headline.', 'default' => true], 'page.show_services' => ['label' => 'Services list', 'help' => 'Components grouped into services.', 'default' => true], 'page.show_component_uptime' => ['label' => 'Uptime bar per component', 'help' => 'The small bar next to each component.', 'default' => true], 'page.show_incidents' => ['label' => 'Incident history', 'help' => 'Incidents grouped per day.', 'default' => true], 'page.show_empty_days' => ['label' => 'Days without incidents', 'help' => 'Shows "No incidents" rather than skipping the day.', 'default' => true], 'page.show_api_link' => ['label' => 'API link in the footer', 'help' => 'Points at the public JSON feed.', 'default' => true], 'page.show_subscribe' => ['label' => '"Get notified" button', 'help' => 'Lets visitors subscribe to incident e-mails. Needs mail settings (Settings → Mail) and the switch on the Subscribers screen.', 'default' => true], ]; public function module(string $key): bool { $default = self::MODULES[$key]['default'] ?? true; // Every default happens to be "on" today; the table is still the source of truth. return Setting::get($key, $default ? '1' : '0') === '1'; // @phpstan-ignore ternary.alwaysTrue } /** @return array */ public function modules(): array { return collect(self::MODULES)->map(fn ($m, $key) => $this->module($key))->all(); } public function name(): string { return Setting::get('brand.name', 'Pharos'); } public function accent(): string { return Setting::get('brand.accent', '#0079d2'); } /** system | light | dark — what the page starts on before a visitor chooses. */ public function theme(): string { $theme = Setting::get('brand.theme', 'system'); return in_array($theme, ['system', 'light', 'dark'], true) ? $theme : 'system'; } public function __construct(protected License $license) {} /** * The paid half is checked when it is *shown*, not only when it is saved: * otherwise a key that is removed, shared or lapsed would leave the branding * behind for good. Uploads stay on disk so a returning key restores them. */ public function licensed(): bool { return $this->license->has(License::FEATURE_BRAND_PACK); } public function creditHidden(): bool { return $this->licensed() && Setting::get('brand.credit_hidden', '0') === '1'; } /** Uploaded logo, or null to fall back to the built-in mark. */ public function logoUrl(): ?string { return $this->licensed() ? $this->assetUrl('brand.logo_path') : null; } /** Second logo for the dark theme; the light one is the fallback. */ public function logoDarkUrl(): ?string { return $this->licensed() ? $this->assetUrl('brand.logo_dark_path') : null; } /** Content-based URLs also refresh assets when the configured release version is stale. */ public function builtInAssetUrl(string $name): string { $path = public_path('brand/'.$name); $revision = is_file($path) ? hash_file('sha256', $path) : (string) config('pharos.version'); return asset('brand/'.$name).'?v='.rawurlencode((string) $revision); } public function faviconUrl(): string { return ($this->licensed() ? $this->assetUrl('brand.favicon_path') : null) ?? $this->builtInAssetUrl('pharos-favicon.svg'); } protected function assetUrl(string $key): ?string { $path = Setting::get($key); // A setting pointing at a file that is gone must not render a broken image. if (! $path || ! Storage::disk('public')->exists($path)) { return null; } return Storage::disk('public')->url($path); } }