diff --git a/modules/Stylist/Http/Controllers/ThemeManagementController.php b/modules/Stylist/Http/Controllers/ThemeManagementController.php index 3481218..3ed8753 100644 --- a/modules/Stylist/Http/Controllers/ThemeManagementController.php +++ b/modules/Stylist/Http/Controllers/ThemeManagementController.php @@ -28,7 +28,13 @@ final class ThemeManagementController extends Controller { $item = $loader->find($theme); - return $views->makeForTheme($item->key(), $item->entryView(), [ + abort_unless( + in_array($item->type(), ['frontend', 'backend'], true), + 422, + 'Only frontend and backend themes can be previewed.' + ); + + return view('stylist::preview', [ 'themePreview' => true, 'previewTheme' => $item, ]); diff --git a/modules/Stylist/Http/Controllers/ThemeManagementController.php.bak-20261020-103617 b/modules/Stylist/Http/Controllers/ThemeManagementController.php.bak-20261020-103617 new file mode 100644 index 0000000..3481218 --- /dev/null +++ b/modules/Stylist/Http/Controllers/ThemeManagementController.php.bak-20261020-103617 @@ -0,0 +1,90 @@ +find($theme); + abort_unless(in_array($item->type(), ['frontend', 'backend'], true), 422, 'Only frontend and backend themes can be activated.'); + + $settings->put($item->type().'_theme', $item->key(), 'site', 'string', true); + + return back()->with('status', $item->name().' is now the active '.$item->type().' theme.'); + } + + public function preview(string $theme, Loader $loader, ThemeViewResolver $views) + { + $item = $loader->find($theme); + + return $views->makeForTheme($item->key(), $item->entryView(), [ + 'themePreview' => true, + 'previewTheme' => $item, + ]); + } + + public function settings(Request $request, string $theme, Loader $loader, ThemeSettings $settings) + { + $item = $loader->find($theme); + $settings->save( + $item, + (array) $request->input('settings', []), + (string) $request->input('variant', 'light'), + $request->string('appearance')->toString() ?: null, + $request->string('accent')->toString() ?: null, + ); + + return back()->with('status', 'Theme settings saved.'); + } + + public function install(Request $request, ThemePackageManager $packages) + { + $request->validate(['theme' => ['required', 'file', 'mimes:zip', 'max:51200']]); + $theme = $packages->install($request->file('theme')); + + return back()->with('status', $theme->name().' installed.'); + } + + public function uninstall(string $theme, Loader $loader, ThemePackageManager $packages) + { + $item = $loader->find($theme); + abort_if(in_array($item->key(), ['sonardesk', 'sonardesk-admin'], true), 403, 'Bundled themes cannot be removed.'); + $settings = app(SiteSettingsRepository::class); + abort_if($settings->get($item->type().'_theme', null, 'site') === $item->key(), 409, 'Deactivate this theme before uninstalling.'); + $packages->uninstall($item); + + return back()->with('status', $item->name().' uninstalled.'); + } + + public function export(string $theme, Loader $loader, ThemePackageManager $packages) + { + $item = $loader->find($theme); + $path = $packages->export($item); + + return response()->download($path)->deleteFileAfterSend(); + } + + public function optimize(string $theme, Loader $loader, ThemeOptimizer $optimizer) + { + $item = $loader->find($theme); + $result = $optimizer->optimize($item); + + return back()->with('status', "Optimised {$result['css']} CSS, {$result['js']} JS and {$result['images']} images."); + } + + public function validateTheme(string $theme, Loader $loader, ThemeAccessibilityValidator $validator) + { + return response()->json(['issues' => $validator->validate($loader->find($theme))]); + } +} diff --git a/modules/Stylist/Providers/StylistServiceProvider.php b/modules/Stylist/Providers/StylistServiceProvider.php index 2491f70..0e76dd5 100644 --- a/modules/Stylist/Providers/StylistServiceProvider.php +++ b/modules/Stylist/Providers/StylistServiceProvider.php @@ -41,6 +41,11 @@ final class StylistServiceProvider extends ServiceProvider public function boot(): void { + \Illuminate\Support\Facades\View::addNamespace( + 'sonardesk-base', + resource_path('views') + ); + $this->registerViews(); $this->registerThemeViews(); $this->registerTranslations(); diff --git a/modules/Stylist/Providers/StylistServiceProvider.php.bak-20261020-103220 b/modules/Stylist/Providers/StylistServiceProvider.php.bak-20261020-103220 new file mode 100644 index 0000000..2491f70 --- /dev/null +++ b/modules/Stylist/Providers/StylistServiceProvider.php.bak-20261020-103220 @@ -0,0 +1,215 @@ +app->register(RouteServiceProvider::class); + + $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); + + $this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class)); + $this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class)); + $this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class)); + } + + public function boot(): void + { + $this->registerViews(); + $this->registerThemeViews(); + $this->registerTranslations(); + $this->registerPublishing(); + $this->registerHelpers(); + $this->registerBlade(); + + if ($this->app->runningInConsole()) { + $this->commands([ + PublishAssetsCommand::class, + ValidateThemeCommand::class, + InspectThemesCommand::class, + FindDuplicateViewsCommand::class, + ThemeDoctorCommand::class, + CreateThemeCommand::class, + ]); + } + + $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); + + View::composer('*', function ($view): void { + if (! $this->app->bound(Stylist::class)) { + return; + } + + $view->with('activeTheme', $this->app->make(Stylist::class)->current()); + }); + + + View::composer('stylist-theme-saasland::*', function ($view): void { + $theme = $this->app->make(Loader::class)->find('saasland'); + $settings = $this->app->make(ThemeSettings::class)->get($theme); + $siteSettings = $this->app->make(SiteSettingsRepository::class)->site(); + $navigation = $this->app->make(FrontendNavigation::class); + $homepage = $this->app->make(FrontendContent::class)->homepage($settings); + + $view->with([ + 'siteSettings' => $siteSettings, + 'siteTitle' => $siteSettings['site_name'], + 'siteDescription' => $siteSettings['site_description'], + 'logoUrl' => $siteSettings['logo_url'] ?: null, + 'facebookUrl' => $siteSettings['facebook_url'] ?: null, + 'xUrl' => $siteSettings['x_url'] ?: null, + 'linkedinUrl' => $siteSettings['linkedin_url'] ?: null, + 'primaryNavigation' => $navigation->primary(), + 'footerNavigation' => $navigation->footer(), + 'themeSettings' => $settings, + ...$homepage, + ]); + }); + } + + private function registerViews(): void + { + $sourcePath = module_path(ModuleInfo::name(), 'Resources/views'); + $viewPath = resource_path('views/modules/'.ModuleInfo::nameLower()); + + $this->publishes([$sourcePath => $viewPath], ['views', 'stylist-module-views']); + $this->loadViewsFrom([$viewPath, $sourcePath], ModuleInfo::nameLower()); + } + + private function registerThemeViews(): void + { + foreach ($this->themeRoots() as $root) { + if (! is_dir($root)) { + logger()->warning('Stylist theme root does not exist', [ + 'root' => $root, + ]); + + continue; + } + + $themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: []; + + foreach ($themeDirectories as $themeDirectory) { + $themeViewsPath = $themeDirectory.'/views'; + + if (! is_dir($themeViewsPath)) { + continue; + } + + $themeKey = strtolower(basename($themeDirectory)); + + // Themes always live outside the module, normally in resources/themes. + // Register the canonical namespace and the legacy namespace as an alias + // to the same application-owned directory. The alias keeps existing + // routes and Blade templates working without reintroducing module themes. + $namespaces = [ + "stylist-theme-{$themeKey}", + "stylist-module-theme-{$themeKey}", + ]; + + $componentsPath = $themeViewsPath.'/components'; + + // The common theme is the application-wide owner of layouts, + // authentication, settings and shared components. Livewire 4 + // resolves its default full-page layout through layouts::app, + // so point that compatibility namespace at the common theme. + if ($themeKey === 'common') { + $layoutsPath = $componentsPath.'/layouts'; + + if (is_dir($layoutsPath)) { + View::addNamespace('layouts', $layoutsPath); + Blade::anonymousComponentPath($layoutsPath, 'layouts'); + } + + View::addNamespace('common', $themeViewsPath); + + if (is_dir($componentsPath)) { + Blade::anonymousComponentPath($componentsPath, 'common'); + } + } + + foreach ($namespaces as $namespace) { + View::addNamespace($namespace, $themeViewsPath); + + if (is_dir($componentsPath)) { + Blade::anonymousComponentPath($componentsPath, $namespace); + } + } + } + } + } + + private function normalisePath(string $path): string + { + return rtrim( + str_replace('\\', '/', realpath($path) ?: $path), + '/' + ); + } + + /** @return array */ + private function themeRoots(): array + { + return array_values(array_unique(array_filter([ + (string) config('stylist.themes_path'), + ]))); + } + + private function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.ModuleInfo::nameLower()); + $sourcePath = module_path(ModuleInfo::name(), 'Resources/lang'); + + $this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower()); + } + + private function registerPublishing(): void + { + $this->publishes([ + module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'), + ], ['config', 'stylist-config']); + + } + + private function registerHelpers(): void + { + foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) { + require_once $filename; + } + } + + private function registerBlade(): void + { + Blade::directive('themeVariables', fn () => 'variables(); ?>'); + Blade::directive('themeStylesheet', fn ($expression) => 'stylesheet('.($expression ?: 'null').'); ?>'); + Blade::directive('themeScript', fn ($expression) => 'script('.($expression ?: 'null').'); ?>'); + Blade::directive('themeSwitcher', fn () => 'switcher(); ?>'); + Blade::directive('themeSeo', fn ($expression) => 'render('.($expression ?: '[]').'); ?>'); + } +} diff --git a/modules/Stylist/Resources/views/index.blade.php b/modules/Stylist/Resources/views/index.blade.php index c667fc3..b078ce6 100644 --- a/modules/Stylist/Resources/views/index.blade.php +++ b/modules/Stylist/Resources/views/index.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.app', ['title' => 'Stylist — SonarDesk Administration']) +@extends('sonardesk-base::layouts.app', ['title' => 'Stylist — SonarDesk Administration']) @section('content') diff --git a/modules/Stylist/Resources/views/index.blade.php.bak-20261020-103220 b/modules/Stylist/Resources/views/index.blade.php.bak-20261020-103220 new file mode 100644 index 0000000..c667fc3 --- /dev/null +++ b/modules/Stylist/Resources/views/index.blade.php.bak-20261020-103220 @@ -0,0 +1,473 @@ +@extends('layouts.app', ['title' => 'Stylist — SonarDesk Administration']) + +@section('content') + +
+ +
+
+ Administration / Appearance +

Stylist

+

+ Manage frontend and administration themes independently. + Install, configure, preview and export complete themes. +

+
+ + + Back to administration + +
+ + @if(session('status')) +
{{ session('status') }}
+ @endif + + @if($errors->any()) +
{{ $errors->first() }}
+ @endif + +
+
+ Installed themes + {{ count($themes) }} + Available designs +
+ +
+ Frontend theme + {{ $activeFrontendTheme }} + Public website +
+ +
+ Backend theme + {{ $activeBackendTheme }} + Administration interface +
+
+ +
+
+
+ Theme library +

Install a theme

+

Upload a self-contained theme package in ZIP format.

+
+
+ +
+ + @csrf + + + + +
+
+ +
+ +
+
+ Appearance +

Installed themes

+

Frontend and backend themes can be activated independently.

+
+
+ +
+ + @foreach($themes as $item) + + @php + $saved = $settingsService->get($item); + $variant = $settingsService->variant($item); + $appearance = $settingsService->appearance($item); + $accent = $settingsService->accent($item); + $issues = $accessibility->validate($item); + + $isActive = + ($item->type() === 'frontend' && $item->key() === $activeFrontendTheme) + || ($item->type() === 'backend' && $item->key() === $activeBackendTheme); + + $isBundled = in_array( + $item->key(), + ['sonardesk', 'sonardesk-admin'], + true + ); + + $themeRoot = realpath(config('stylist.themes_path')); + $themePath = realpath($item->path()); + + $isInstalled = $themeRoot + && $themePath + && str_starts_with( + $themePath, + rtrim($themeRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR + ); + @endphp + +
+ +
+ + @if($item->preview() && $item->has($item->preview())) + + {{ $item->name() }} preview + + @else + +
+ ◈ + {{ $item->name() }} + No preview image available +
+ + @endif + +
+ +
+ +
+ +
+ + {{ ucfirst($item->type()) }} theme + + +

{{ $item->name() }}

+ +

+ Version {{ $item->version() }} + @if($item->author()) + · {{ $item->author() }} + @endif +

+
+ + @if($isActive) + + Active + + @endif + +
+ + @if($item->description()) +

+ {{ $item->description() }} +

+ @endif + + @if($item->parent()) +

+ Based on {{ $item->parent() }} +

+ @endif + +
+ + @if(in_array($item->type(), ['frontend', 'backend'], true)) + + @unless($isActive) + +
+ @csrf + + +
+ + @endunless + + + Preview + + + @endif + + + Export ZIP + + +
+ @csrf + + +
+ +
+ +
+ + + Theme configuration + ⌄ + + +
+ + @csrf + @method('PUT') + + @if($item->variants()) + + + + @endif + + @if($item->supports('dark_mode') || $item->appearances()) + + + + @endif + + @if($item->accents()) + +
+ Accent colour + + @foreach($item->accents() as $name => $definition) + + + + @endforeach + +
+ + @endif + + @foreach($item->settingsSchema() as $key => $definition) + + @php + $definition = is_array($definition) + ? $definition + : ['default' => $definition]; + + $fieldType = $definition['type'] ?? 'text'; + $fieldLabel = $definition['label'] ?? str($key)->headline(); + $fieldValue = $saved[$key] ?? ($definition['default'] ?? ''); + @endphp + +
+ + + + @switch($fieldType) + + @case('color') + + + @break + + @case('textarea') + + + @break + + @case('checkbox') + + + + + @break + + @default + + + + @endswitch + +
+ + @endforeach + + + +
+ +
+ + + +
+ +
+ + @endforeach + +
+ +
+ +
+ + + +@endsection diff --git a/modules/Stylist/Resources/views/preview.blade.php b/modules/Stylist/Resources/views/preview.blade.php new file mode 100644 index 0000000..e4a36ba --- /dev/null +++ b/modules/Stylist/Resources/views/preview.blade.php @@ -0,0 +1,181 @@ + + + + + +{{ $previewTheme->name() }} — Theme Preview + + + + +
+
+ Stylist — Theme Preview + + {{ $previewTheme->name() }} + · Version {{ $previewTheme->version() }} + · {{ ucfirst($previewTheme->type()) }} + +
+ ← Back to Stylist +
+ + + +
+ @if($previewTheme->type() === 'backend') + Administration / Dashboard +

Administration overview

+

+ Preview the administration interface using representative data. +

+ +
+

128

Licences

+

116

Active licences

+

184

Installations

+

92

Portal users

+
+ +
+
+

Release management

+

Manage published software releases.

+ Manage releases +
+
+

Theme management

+

Configure the appearance of SonarDesk.

+ Manage themes +
+
+ @else + SonarDesk for macOS +

Code quality at a glance.

+

+ A native macOS dashboard for monitoring SonarQube projects, + quality gates, vulnerabilities and technical debt. +

+ + Explore SonarDesk + +
+
+

Quality gates

+

Monitor project quality and identify failing checks.

+
+
+

Issues

+

Review bugs, vulnerabilities and code smells.

+
+
+

Reports

+

Export project reports for further analysis.

+
+
+ @endif +
+ + + + + diff --git a/resources/themes/sonardesk-admin/views/admin/index.blade.php b/resources/themes/sonardesk-admin/views/admin/index.blade.php index 61766a6..29bdf82 100755 --- a/resources/themes/sonardesk-admin/views/admin/index.blade.php +++ b/resources/themes/sonardesk-admin/views/admin/index.blade.php @@ -1,6 +1,6 @@ @extends('layouts.app') @section('content')
Admin

Licensing administration

Manage licences, activations and customer support.

-
{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

-
Manage releasesManage licencesManage usersManage contentSEO & searchSupport inbox
+
{{ $licenseCount ?? '—' }}

Licences

{{ $activeCount ?? '—' }}

Not revoked

{{ $activationCount ?? '—' }}

Active installations

{{ $userCount ?? '—' }}

Portal users

+
Manage releasesManage licencesManage usersManage contentManage themesSEO & searchSupport inbox
@endsection diff --git a/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103348 b/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103348 new file mode 100644 index 0000000..61766a6 --- /dev/null +++ b/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103348 @@ -0,0 +1,6 @@ +@extends('layouts.app') +@section('content') +
Admin

Licensing administration

Manage licences, activations and customer support.

+
{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

+
Manage releasesManage licencesManage usersManage contentSEO & searchSupport inbox
+@endsection diff --git a/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103455 b/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103455 new file mode 100644 index 0000000..dd50ff8 --- /dev/null +++ b/resources/themes/sonardesk-admin/views/admin/index.blade.php.bak-20261020-103455 @@ -0,0 +1,6 @@ +@extends('layouts.app') +@section('content') +
Admin

Licensing administration

Manage licences, activations and customer support.

+
{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

+
Manage releasesManage licencesManage usersManage contentManage themesSEO & searchSupport inbox
+@endsection diff --git a/resources/themes/sonardesk/views/portal/welcome.blade.php b/resources/themes/sonardesk/views/portal/welcome.blade.php index c56a643..0a69755 100755 --- a/resources/themes/sonardesk/views/portal/welcome.blade.php +++ b/resources/themes/sonardesk/views/portal/welcome.blade.php @@ -44,7 +44,7 @@
See pricing and start trial
-
Desktop distribution

Ready when you are.

Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.

@if($latestRelease)Latest stableVersion {{ $latestRelease->version }}Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}
SHA-256{{ Str::limit($latestRelease->sha256, 34) }}
@elseSonarDesk releasesDistribution centrePublished builds will appear here automatically.@endif
+
Desktop distribution

Ready when you are.

Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.

@if($latestRelease ?? null)Latest stableVersion {{ $latestRelease->version }}Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}
SHA-256{{ Str::limit($latestRelease->sha256, 34) }}
@elseSonarDesk releasesDistribution centrePublished builds will appear here automatically.@endif
SonarDesk for macOS

Make SonarQube easier to keep an eye on.

Try SonarDesk for seven days and connect it to your existing SonarQube environment.

Start free trial
@endsection diff --git a/resources/themes/sonardesk/views/portal/welcome.blade.php.bak-20261020-103534 b/resources/themes/sonardesk/views/portal/welcome.blade.php.bak-20261020-103534 new file mode 100644 index 0000000..c56a643 --- /dev/null +++ b/resources/themes/sonardesk/views/portal/welcome.blade.php.bak-20261020-103534 @@ -0,0 +1,50 @@ +@extends('layouts.app', [ + 'title' => 'SonarDesk — Native SonarQube monitoring for macOS', + 'description' => 'SonarDesk is a native macOS companion for SonarQube: monitor quality gates, issues, measures and project health from one focused desktop workspace.' +]) +@section('content') +
+ SonarQube, without the browser tab sprawl +

Your code quality.
At home on the Mac.

+

SonarDesk is a focused native macOS companion for developers and teams using SonarQube. See project health, quality gates, bugs, vulnerabilities, code smells and measures in one fast desktop workspace.

+ +

Direct download for macOS · Secure licence activation · Cancel anytime

+
+ +
+ Native macOS appMultiple SonarQube serversKeychain token storageIssue detail workflow +
+ +
+
Built for daily engineering work

See the signal. Open the issue. Get back to code.

SonarDesk keeps the information you check most often close at hand, without replacing SonarQube or changing your existing workflow.

+
+
01

Project health at a glance

Review configured projects, quality gate state and important measures from a clean Mac dashboard.

+
02

Issue details that are useful

Open bugs, vulnerabilities and code smells to see the context you need before switching back to the codebase.

+
03

Focused filtering

Work through issue lists with practical filters and server-side pagination rather than fighting a giant browser view.

+
04

Multiple servers

Keep the SonarQube environments you work with available from one desktop application.

+
05

macOS security

SonarQube access tokens are stored in the macOS Keychain. SonarDesk respects the permissions of the token you provide.

+
06

Native workflow

A purpose-built macOS interface with familiar navigation, desktop behaviour and a focused experience for engineering teams.

+
+
+ +
+
Designed to complement SonarQube

Keep SonarQube as the source of truth.

SonarDesk is a desktop companion, not a replacement. It connects to your SonarQube server using your credentials and presents the project and issue information you need for everyday monitoring.

+
Quality gates and measures
Bugs, vulnerabilities and code smells
Project and issue navigation
Secure token storage in Keychain
Customer portal for licences and activations
+
+ +
+
Straightforward pricing

Start small. Upgrade when you need more Macs.

Every plan includes the full SonarDesk application. Professional increases the activation allowance for people working across multiple Macs.

+
+
Personal

For one Mac

£24.99 / year

or £2.99 monthly

  • Full SonarDesk feature set
  • 1 active Mac
  • 7-day free trial
  • Licence key delivered by email
  • Customer licence portal
+ +
+ +
+ +
Desktop distribution

Ready when you are.

Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.

@if($latestRelease)Latest stableVersion {{ $latestRelease->version }}Build {{ $latestRelease->build_number }} · {{ $latestRelease->formatted_size }} · macOS {{ $latestRelease->minimum_macos ?: 'supported versions' }}
SHA-256{{ Str::limit($latestRelease->sha256, 34) }}
@elseSonarDesk releasesDistribution centrePublished builds will appear here automatically.@endif
+ +
SonarDesk for macOS

Make SonarQube easier to keep an eye on.

Try SonarDesk for seven days and connect it to your existing SonarQube environment.

Start free trial
+@endsection diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index dd50ff8..29bdf82 100755 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -1,6 +1,6 @@ @extends('layouts.app') @section('content')
Admin

Licensing administration

Manage licences, activations and customer support.

-
{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

+
{{ $licenseCount ?? '—' }}

Licences

{{ $activeCount ?? '—' }}

Not revoked

{{ $activationCount ?? '—' }}

Active installations

{{ $userCount ?? '—' }}

Portal users

Manage releasesManage licencesManage usersManage contentManage themesSEO & searchSupport inbox
@endsection diff --git a/resources/views/admin/index.blade.php.bak-20261020-103455 b/resources/views/admin/index.blade.php.bak-20261020-103455 new file mode 100644 index 0000000..dd50ff8 --- /dev/null +++ b/resources/views/admin/index.blade.php.bak-20261020-103455 @@ -0,0 +1,6 @@ +@extends('layouts.app') +@section('content') +
Admin

Licensing administration

Manage licences, activations and customer support.

+
{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

+
Manage releasesManage licencesManage usersManage contentManage themesSEO & searchSupport inbox
+@endsection