Navbar System Rebuild Plan¶
Status: Planning Phase Last Updated: 2026-01-04 Decision: Option A (Normalized Tables) + DB-based visibility_rules
Table of Contents¶
- Problem Statement
- Current Architecture Analysis
- Database Redesign
- Menu Visibility Rules
- Settings Normalization
- Service Layer Rebuild
- View Layer Simplification
- Migration Strategy
- Implementation Phases
Problem Statement¶
Performance Metrics (Current)¶
| Metric | Value | Target |
|---|---|---|
BladeCache::navMenu calls |
500/request | 1 |
| Total render time | 7,843ms | <200ms |
| Memory delta | +386MB | <20MB |
| Database queries | 2,681 | <15 |
Root Causes¶
- MenuService.php is 4,311 lines - Acts as a PHP-based database with 224+ menu definitions
- Per-item visibility checks -
PlanHelper::planMenuCheck()called for each menu item in blade loop - Circular service calls -
planMenuCheck()callsMenuService::generate()which triggers more checks - Settings bloat -
settings_twohas 40+ columns, many unused (e.g.,plagiarism_key) - 77 settings migrations - Incremental column additions created schema sprawl
Current Architecture Analysis¶
Call Chain Causing 500 Renders¶
HTTP Request
└── app.blade.php
└── navbar.blade.php
└── BladeCache::navMenu()
└── menu.blade.php
└── @foreach ($items as $item) // ~100+ items
└── PlanHelper::planMenuCheck() // Called 100+ times
├── MenuService::planAiToolsMenu()
│ └── MenuService::generate() // Recursive!
└── MenuService::planFeatureMenu()
└── MenuService::generate() // Recursive!
Files Involved¶
| File | Lines | Problem |
|---|---|---|
app/Services/Common/MenuService.php |
4,311 | PHP-as-database, 224+ menu defs |
app/Helpers/Classes/PlanHelper.php |
~150 | Per-item DB lookups |
app/Caches/BladeCache.php |
167 | Per-request cache never reset |
app/Providers/ViewServiceProvider.php |
- | PlanComposer on both navbar AND menu |
resources/views/.../menu.blade.php |
40 | Per-item planMenuCheck calls |
Settings Table Sprawl¶
settings table: Invoice, payment, social login, SMTP, OpenAI (89 lines initial + migrations)
settings_two table: Started with 7 columns, grew to 40+ via 16 migrations including:
- stable_diffusion_* - Image generation
- bedrock_* - AWS Bedrock config
- fine_tune_list - JSON blob of OpenAI fine-tunes
- plagiarism_key - UNUSED (null in sample data)
- liquid_license_* - Legacy license keys (no longer needed)
Database Redesign¶
Decision: Option A - Normalized Tables¶
Chosen for: Readability, maintainability, clear domain boundaries
New Schema Overview¶
┌─────────────────────────────────────────────────────────────────┐
│ SETTINGS DOMAIN │
├─────────────────────────────────────────────────────────────────┤
│ settings_branding │ Logos, site name, theme │
│ settings_ai │ Bedrock config, model defaults │
│ settings_features │ Feature flags, limits │
│ settings_integrations│ Social login, SMTP, payment gateways │
│ api_credentials │ Encrypted API keys (separate table) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ MENU DOMAIN │
├─────────────────────────────────────────────────────────────────┤
│ menus │ Menu items with visibility_rules JSON │
│ menu_groups │ Logical groupings (if needed) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ AI/ML DOMAIN │
├─────────────────────────────────────────────────────────────────┤
│ fine_tunes │ Bedrock-ready fine-tune configs │
└─────────────────────────────────────────────────────────────────┘
Table: settings_branding¶
CREATE TABLE settings_branding (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
site_name VARCHAR(255) NOT NULL DEFAULT 'Vellocity',
site_url VARCHAR(255) NOT NULL,
site_email VARCHAR(255) NULL,
-- Logos (consolidated from settings + settings_two)
logo_path VARCHAR(255) NULL,
logo_dark_path VARCHAR(255) NULL,
logo_2x_path VARCHAR(255) NULL,
logo_dark_2x_path VARCHAR(255) NULL,
logo_dashboard_path VARCHAR(255) NULL,
logo_dashboard_dark_path VARCHAR(255) NULL,
logo_collapsed_path VARCHAR(255) NULL,
logo_collapsed_dark_path VARCHAR(255) NULL,
favicon_path VARCHAR(255) NULL,
-- Theme
theme VARCHAR(50) DEFAULT 'default',
dash_theme VARCHAR(50) DEFAULT 'default',
-- SEO
meta_title TEXT NULL,
meta_description TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Table: settings_ai¶
CREATE TABLE settings_ai (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
-- AWS Bedrock (primary)
bedrock_access_model ENUM('platform', 'user') DEFAULT 'platform',
bedrock_only_mode BOOLEAN DEFAULT TRUE,
bedrock_default_guardrail_id VARCHAR(100) NULL,
bedrock_default_guardrail_version VARCHAR(20) DEFAULT 'DRAFT',
bedrock_guardrails_enabled BOOLEAN DEFAULT FALSE,
bedrock_require_guardrails BOOLEAN DEFAULT FALSE,
bedrock_image_model VARCHAR(100) DEFAULT 'amazon.titan-image-generator-v2:0',
-- Model defaults
default_chat_model VARCHAR(100) NULL,
default_image_model VARCHAR(100) NULL,
default_tts_provider ENUM('openai', 'elevenlabs', 'google') DEFAULT 'openai',
default_stream_server ENUM('backend', 'frontend') DEFAULT 'backend',
-- Image storage
ai_image_storage ENUM('local', 's3') DEFAULT 's3',
-- SEO AI
seo_use_bedrock BOOLEAN DEFAULT TRUE,
seo_bedrock_model VARCHAR(100) DEFAULT 'bedrock-nova-2-pro',
seo_enhance_with_llm BOOLEAN DEFAULT TRUE,
-- Feature flags
feature_tts_google BOOLEAN DEFAULT FALSE,
feature_tts_openai BOOLEAN DEFAULT TRUE,
feature_tts_elevenlabs BOOLEAN DEFAULT FALSE,
feature_ai_video BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Table: settings_features¶
CREATE TABLE settings_features (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
-- Usage limits
daily_limit_enabled BOOLEAN DEFAULT FALSE,
allowed_images_count INT DEFAULT 100,
daily_voice_limit_enabled BOOLEAN DEFAULT FALSE,
allowed_voice_count INT DEFAULT 100,
-- Chatbot
chatbot_status ENUM('frontend', 'backend', 'disabled') DEFAULT 'frontend',
chatbot_template INT DEFAULT 1,
chatbot_position ENUM('bottom-right', 'bottom-left') DEFAULT 'bottom-right',
chatbot_login_require BOOLEAN DEFAULT TRUE,
chatbot_rate_limit INT DEFAULT 20,
chatbot_show_timestamp BOOLEAN DEFAULT TRUE,
-- Affiliates
feature_affiliates BOOLEAN DEFAULT FALSE,
affiliate_commission_percentage DECIMAL(5,2) DEFAULT 0,
affiliate_plan_restriction BOOLEAN DEFAULT FALSE,
-- Registration
register_active BOOLEAN DEFAULT TRUE,
default_country VARCHAR(100) DEFAULT 'United States',
-- Recaptcha
recaptcha_enabled BOOLEAN DEFAULT FALSE,
recaptcha_login BOOLEAN DEFAULT FALSE,
recaptcha_register BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Table: settings_integrations¶
CREATE TABLE settings_integrations (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
-- Payment: Stripe
stripe_active BOOLEAN DEFAULT FALSE,
stripe_base_url VARCHAR(255) DEFAULT 'https://api.stripe.com',
default_currency VARCHAR(10) DEFAULT 'USD',
tax_rate DECIMAL(5,2) NULL,
-- Payment: Bank Transfer
bank_transfer_active BOOLEAN DEFAULT FALSE,
bank_transfer_instructions TEXT NULL,
bank_transfer_informations TEXT NULL,
-- Social: Facebook
facebook_active BOOLEAN DEFAULT FALSE,
facebook_redirect_url VARCHAR(255) NULL,
-- Social: Google
google_active BOOLEAN DEFAULT FALSE,
google_redirect_url VARCHAR(255) NULL,
-- Social: GitHub
github_active BOOLEAN DEFAULT FALSE,
github_redirect_url VARCHAR(255) NULL,
-- SMTP
smtp_host VARCHAR(255) NULL,
smtp_port INT NULL,
smtp_username VARCHAR(255) NULL,
smtp_email VARCHAR(255) NULL,
smtp_sender_name VARCHAR(255) NULL,
smtp_encryption ENUM('TLS', 'SSL', 'none') DEFAULT 'TLS',
-- Analytics
google_analytics_active BOOLEAN DEFAULT FALSE,
google_analytics_code TEXT NULL,
-- Localization
languages VARCHAR(255) DEFAULT 'en',
languages_default VARCHAR(10) DEFAULT 'en',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Table: api_credentials¶
CREATE TABLE api_credentials (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
provider VARCHAR(50) NOT NULL, -- 'stripe', 'openai', 'bedrock', 'elevenlabs', etc.
-- Encrypted storage
api_key_encrypted TEXT NULL,
api_secret_encrypted TEXT NULL,
additional_data_encrypted JSON NULL, -- For provider-specific fields
-- Metadata
is_active BOOLEAN DEFAULT TRUE,
last_validated_at TIMESTAMP NULL,
validation_status ENUM('valid', 'invalid', 'unknown') DEFAULT 'unknown',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_provider (provider)
);
-- Example providers:
-- 'stripe' -> api_key, api_secret
-- 'openai' -> api_key (deprecated, migrate to bedrock)
-- 'bedrock' -> handled via IAM, no keys stored
-- 'stable_diffusion' -> api_key (deprecated)
-- 'elevenlabs' -> api_key
-- 'serper' -> api_key
-- 'unsplash' -> api_key
-- 'facebook' -> api_key, api_secret
-- 'google' -> api_key, api_secret
-- 'github' -> api_key, api_secret
-- 'recaptcha' -> site_key, secret_key
Table: fine_tunes (Bedrock-Ready)¶
CREATE TABLE fine_tunes (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
-- Identification
provider ENUM('openai', 'bedrock') NOT NULL DEFAULT 'bedrock',
external_id VARCHAR(255) NOT NULL, -- file-xxx for OpenAI, arn:aws:... for Bedrock
-- Metadata
title VARCHAR(255) NOT NULL,
description TEXT NULL,
-- File info
filename VARCHAR(255) NULL,
file_size_bytes BIGINT NULL,
-- Status
status ENUM('pending', 'processing', 'ready', 'failed') DEFAULT 'pending',
status_details TEXT NULL,
-- Bedrock-specific
bedrock_model_arn VARCHAR(500) NULL,
bedrock_base_model VARCHAR(100) NULL, -- e.g., 'anthropic.claude-3-haiku'
bedrock_training_job_arn VARCHAR(500) NULL,
-- OpenAI legacy (for migration)
openai_file_id VARCHAR(100) NULL,
openai_file_object JSON NULL, -- Preserved for migration reference
-- Usage tracking
is_active BOOLEAN DEFAULT TRUE,
usage_count INT DEFAULT 0,
last_used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_provider_status (provider, status),
INDEX idx_active (is_active)
);
Menu Visibility Rules¶
Enhanced menus Table¶
ALTER TABLE menus ADD COLUMN visibility_rules JSON NULL AFTER is_active;
ALTER TABLE menus ADD COLUMN full_path VARCHAR(500) NULL;
ALTER TABLE menus ADD INDEX idx_menu_nav (parent_id, is_active, `order`);
ALTER TABLE menus ADD INDEX idx_menu_key (key);
visibility_rules JSON Schema¶
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"require_auth": {
"type": "boolean",
"description": "User must be authenticated"
},
"require_admin": {
"type": "boolean",
"description": "User must be admin"
},
"require_permission": {
"type": "string",
"description": "Specific permission key required"
},
"require_plan_feature": {
"type": "string",
"description": "Plan must include this feature key"
},
"require_plan_ai_tool": {
"type": "string",
"description": "Plan must include this AI tool key"
},
"require_extension": {
"type": "string",
"description": "Extension must be installed and active"
},
"require_route": {
"type": "string",
"description": "Named route must exist"
},
"require_setting": {
"type": "object",
"properties": {
"key": { "type": "string" },
"value": { "type": ["string", "boolean", "number"] }
},
"description": "Setting must have specific value"
},
"active_conditions": {
"type": "array",
"items": { "type": "string" },
"description": "Route patterns that mark this item as active"
},
"show_badge": {
"type": "string",
"enum": ["new", "beta", "pro"],
"description": "Badge to display"
}
}
}
Example visibility_rules Values¶
-- Dashboard (visible to all authenticated users)
UPDATE menus SET visibility_rules = '{"require_auth": true}'
WHERE key = 'dashboard';
-- Admin Settings (admin only)
UPDATE menus SET visibility_rules = '{"require_admin": true}'
WHERE key = 'admin_settings';
-- AI Chat (requires extension + plan feature)
UPDATE menus SET visibility_rules = '{
"require_auth": true,
"require_extension": "ai-chat-pro",
"require_plan_feature": "ai_chat",
"active_conditions": ["dashboard.user.openai.chat.*"]
}' WHERE key = 'ai_chat';
-- Creative Suite (extension-based)
UPDATE menus SET visibility_rules = '{
"require_extension": "creative-suite",
"show_badge": "new"
}' WHERE key = 'creative_suite';
-- Affiliates (setting + user status)
UPDATE menus SET visibility_rules = '{
"require_setting": {"key": "feature_affiliates", "value": true},
"require_user_affiliate_status": 1
}' WHERE key = 'affiliate_menu';
Moving MenuService::data() to Database¶
The current 4,311-line MenuService::data() method contains 224+ menu definitions like:
'dashboard' => [
'parent_key' => null,
'key' => 'dashboard',
'route' => 'dashboard.user.index',
'label' => 'Dashboard',
'icon' => 'tabler-layout-2',
'order' => 2,
'is_active' => true,
'type' => 'item',
'active_condition' => ['dashboard.user.index'],
'show_condition' => true,
],
Migration Strategy:
- Create a migration that reads
MenuService::data()output - For each item, create/update menu record with:
- Basic fields directly to columns
active_condition+show_condition->visibility_rulesJSON- Mark MenuService::data() as deprecated
- New MenuService loads from DB only
Settings Normalization¶
Migration Path: settings_two Decomposition¶
| Current Column | Target Table | Notes |
|---|---|---|
theme |
settings_branding |
|
stable_diffusion_api_key |
api_credentials |
provider='stable_diffusion' |
stable_diffusion_default_model |
REMOVE | Deprecated |
google_recaptcha_status |
settings_features |
As recaptcha_enabled |
google_recaptcha_site_key |
api_credentials |
provider='recaptcha' |
google_recaptcha_secret_key |
api_credentials |
provider='recaptcha' |
languages |
settings_integrations |
|
languages_default |
settings_integrations |
|
liquid_license_type |
REMOVE | Legacy, no longer used |
liquid_license_domain_key |
REMOVE | Legacy, no longer used |
openai_default_stream_server |
settings_ai |
As default_stream_server |
ai_image_storage |
settings_ai |
|
stablediffusion_default_language |
REMOVE | Deprecated |
stablediffusion_default_model |
REMOVE | Deprecated |
unsplash_api_key |
api_credentials |
provider='unsplash' |
dalle |
REMOVE | Deprecated (moving to Bedrock) |
daily_limit_enabled |
settings_features |
|
allowed_images_count |
settings_features |
|
daily_voice_limit_enabled |
settings_features |
|
allowed_voice_count |
settings_features |
|
serper_api_key |
api_credentials |
provider='serper' |
elevenlabs_api_key |
api_credentials |
provider='elevenlabs' |
feature_tts_* |
settings_ai |
|
fine_tune_list |
fine_tunes table |
JSON -> normalized rows |
chatbot_* |
settings_features |
|
feature_ai_video |
settings_ai |
|
stablediffusion_bedrock_model |
settings_ai |
As bedrock_image_model |
seo_* |
settings_ai |
|
bedrock_* |
settings_ai |
|
plagiarism_key |
REMOVE | Never used (always null) |
Columns to Remove¶
-- Dead columns (never used or deprecated)
ALTER TABLE settings_two
DROP COLUMN plagiarism_key,
DROP COLUMN liquid_license_type,
DROP COLUMN liquid_license_domain_key,
DROP COLUMN stable_diffusion_default_model,
DROP COLUMN stablediffusion_default_language,
DROP COLUMN stablediffusion_default_model,
DROP COLUMN dalle;
Backward Compatibility Layer¶
During transition, create a SettingsLegacy model that reads from new tables but presents the old interface:
class SettingsLegacy
{
// Maps old property names to new table.column
private static array $mappings = [
'theme' => ['settings_branding', 'theme'],
'bedrock_only_mode' => ['settings_ai', 'bedrock_only_mode'],
// ... etc
];
public function __get($name)
{
if (isset(self::$mappings[$name])) {
[$table, $column] = self::$mappings[$name];
return DB::table($table)->value($column);
}
return null;
}
}
Service Layer Rebuild¶
New: NavbarService¶
<?php
namespace App\Services\Navbar;
use App\DTOs\MenuDTO;
use App\DTOs\UserContextDTO;
use App\Models\Common\Menu;
use App\Models\User;
use Illuminate\Support\Collection;
class NavbarService
{
private ?MenuDTO $cachedMenu = null;
/**
* Get menu for user - single entry point
* All visibility filtering happens here, NOT in blade
*/
public function getMenuForUser(?User $user): MenuDTO
{
if ($this->cachedMenu !== null) {
return $this->cachedMenu;
}
$context = $this->buildUserContext($user);
$rawMenu = $this->loadMenuTree();
$filteredMenu = $this->applyVisibilityRules($rawMenu, $context);
$this->cachedMenu = new MenuDTO(
items: $filteredMenu,
context: $context,
cacheKey: $this->generateCacheKey($context)
);
return $this->cachedMenu;
}
/**
* Load menu tree - SINGLE QUERY with eager loading
*/
private function loadMenuTree(): Collection
{
return Menu::query()
->with(['children' => fn($q) => $q
->where('is_active', true)
->orderBy('order')
])
->whereNull('parent_id')
->where('is_active', true)
->orderBy('order')
->get();
}
/**
* Build user context ONCE per request
*/
private function buildUserContext(?User $user): UserContextDTO
{
if (!$user) {
return new UserContextDTO(
isAuthenticated: false,
isAdmin: false,
planFeatures: [],
planAiTools: [],
permissions: [],
activeExtensions: []
);
}
$plan = $user->activePlan();
return new UserContextDTO(
isAuthenticated: true,
isAdmin: $user->isAdmin(),
planId: $plan?->id,
planFeatures: $plan?->getFeatureKeys() ?? [],
planAiTools: $plan?->getAiToolKeys() ?? [],
permissions: $user->getAllPermissionKeys(),
activeExtensions: $this->getActiveExtensions(),
affiliateStatus: $user->affiliate_status
);
}
/**
* Apply visibility rules IN MEMORY (no more per-item DB calls)
*/
private function applyVisibilityRules(Collection $items, UserContextDTO $context): array
{
return $items
->filter(fn($item) => $this->isVisible($item, $context))
->map(function ($item) use ($context) {
$data = $item->toArray();
if (!empty($data['children'])) {
$data['children'] = collect($data['children'])
->filter(fn($child) => $this->isVisible($child, $context))
->values()
->toArray();
}
return $data;
})
->values()
->toArray();
}
/**
* Check visibility using DB-stored rules
*/
private function isVisible($item, UserContextDTO $context): bool
{
$rules = $item->visibility_rules ?? [];
if (empty($rules)) {
return true; // No rules = always visible
}
// Check each rule type
if (($rules['require_auth'] ?? false) && !$context->isAuthenticated) {
return false;
}
if (($rules['require_admin'] ?? false) && !$context->isAdmin) {
return false;
}
if ($permission = ($rules['require_permission'] ?? null)) {
if (!in_array($permission, $context->permissions)) {
return false;
}
}
if ($feature = ($rules['require_plan_feature'] ?? null)) {
if (!in_array($feature, $context->planFeatures)) {
return false;
}
}
if ($tool = ($rules['require_plan_ai_tool'] ?? null)) {
if (!in_array($tool, $context->planAiTools)) {
return false;
}
}
if ($extension = ($rules['require_extension'] ?? null)) {
if (!in_array($extension, $context->activeExtensions)) {
return false;
}
}
if ($route = ($rules['require_route'] ?? null)) {
if (!Route::has($route)) {
return false;
}
}
if ($setting = ($rules['require_setting'] ?? null)) {
$actualValue = setting($setting['key']);
if ($actualValue !== $setting['value']) {
return false;
}
}
return true;
}
}
New: DTOs¶
<?php
namespace App\DTOs;
readonly class MenuDTO
{
public function __construct(
public array $items,
public UserContextDTO $context,
public string $cacheKey
) {}
}
readonly class UserContextDTO
{
public function __construct(
public bool $isAuthenticated,
public bool $isAdmin,
public ?int $planId = null,
public array $planFeatures = [],
public array $planAiTools = [],
public array $permissions = [],
public array $activeExtensions = [],
public ?int $affiliateStatus = null
) {}
}
Deprecated: MenuService::data()¶
After migration, the 4,311-line method becomes:
/**
* @deprecated Use NavbarService::getMenuForUser() instead
* This method is kept temporarily for migration reference only
*/
public function data(): array
{
throw new \RuntimeException(
'MenuService::data() is deprecated. Use NavbarService::getMenuForUser() instead.'
);
}
View Layer Simplification¶
Current Structure (14 files)¶
resources/views/default/components/navbar/
├── divider.blade.php
├── dropdown/
│ ├── dropdown.blade.php
│ ├── item.blade.php
│ └── link.blade.php
├── item.blade.php
├── label.blade.php
├── link.blade.php
├── navbar.blade.php
└── partials/
├── credit-for-menu.blade.php
├── link-markup.blade.php
└── types/
├── divider.blade.php
├── item-dropdown.blade.php
├── item.blade.php
└── label.blade.php
Target Structure (4 files)¶
resources/views/default/components/navbar/
├── navbar.blade.php # Container, logo, nav wrapper
├── menu-tree.blade.php # Recursive menu renderer
├── menu-item.blade.php # Single item (all types: item, dropdown, label, divider)
└── credits.blade.php # Credit display section
New navbar.blade.php¶
@inject('navbarService', 'App\Services\Navbar\NavbarService')
@php
$menuData = $navbarService->getMenuForUser(auth()->user());
@endphp
<aside class="lqd-navbar ...">
<div class="lqd-navbar-inner ...">
{{-- Logo section --}}
<div class="lqd-navbar-logo ...">
<a href="{{ route('dashboard.index') }}">
<x-branding.logo variant="dashboard" />
</a>
</div>
{{-- Menu - NO BladeCache, NO per-item checks --}}
<nav class="lqd-navbar-nav" id="navbar-menu">
<ul class="lqd-navbar-ul">
<x-navbar.menu-tree :items="$menuData->items" />
</ul>
</nav>
{{-- Credits --}}
<x-navbar.credits />
</div>
</aside>
New menu-tree.blade.php¶
@props(['items', 'depth' => 0])
@foreach ($items as $item)
<x-navbar.menu-item :item="$item" :depth="$depth" />
@endforeach
New menu-item.blade.php¶
@props(['item', 'depth' => 0])
@php
$type = data_get($item, 'type', 'item');
$hasChildren = !empty($item['children']);
$isActive = $this->isActive($item);
@endphp
@switch($type)
@case('label')
<li class="lqd-navbar-label ...">
{{ __($item['label']) }}
</li>
@break
@case('divider')
<li class="lqd-navbar-divider ..."></li>
@break
@default
@if ($hasChildren)
{{-- Dropdown --}}
<li class="lqd-navbar-item lqd-navbar-dropdown ..." x-data="{ open: false }">
<button @click="open = !open" class="...">
@if ($item['icon'])
<x-dynamic-component :component="$item['icon']" class="..." />
@endif
<span>{{ __($item['label']) }}</span>
<x-tabler-chevron-down class="..." />
</button>
<ul x-show="open" class="...">
<x-navbar.menu-tree :items="$item['children']" :depth="$depth + 1" />
</ul>
</li>
@else
{{-- Regular item --}}
<li class="lqd-navbar-item {{ $isActive ? 'active' : '' }} ...">
<a href="{{ $item['route'] ? route($item['route'], $item['params'] ?? []) : '#' }}" class="...">
@if ($item['icon'])
<x-dynamic-component :component="$item['icon']" class="..." />
@endif
<span>{{ __($item['label']) }}</span>
@if ($badge = data_get($item, 'visibility_rules.show_badge'))
<span class="badge badge-{{ $badge }}">{{ $badge }}</span>
@endif
</a>
</li>
@endif
@endswitch
Migration Strategy¶
Phase Order¶
Phase 1: Foundation (No breaking changes)
├── Create new tables (empty)
├── Add visibility_rules column to menus
├── Create NavbarService (unused yet)
└── Create DTOs
Phase 2: Data Migration (Background)
├── Migrate settings to new tables
├── Migrate fine_tune_list to fine_tunes table
├── Populate visibility_rules from MenuService::data()
└── Create backward compatibility layer
Phase 3: Service Cutover
├── Switch NavbarService to production
├── Update blade templates
├── Deprecate old services
└── Remove BladeCache::navMenu() complexity
Phase 4: Cleanup
├── Drop deprecated columns
├── Remove legacy code
├── Drop old settings tables (after verification)
└── Final performance validation
Implementation Phases¶
Phase 1: Foundation (Week 1)¶
Migrations to Create:
2026_01_05_000001_create_settings_branding_table.php2026_01_05_000002_create_settings_ai_table.php2026_01_05_000003_create_settings_features_table.php2026_01_05_000004_create_settings_integrations_table.php2026_01_05_000005_create_api_credentials_table.php2026_01_05_000006_create_fine_tunes_table.php2026_01_05_000007_add_visibility_rules_to_menus_table.php
New Files:
app/Services/Navbar/NavbarService.phpapp/DTOs/MenuDTO.phpapp/DTOs/UserContextDTO.phpapp/Models/Settings/SettingsBranding.phpapp/Models/Settings/SettingsAI.phpapp/Models/Settings/SettingsFeatures.phpapp/Models/Settings/SettingsIntegrations.phpapp/Models/ApiCredential.phpapp/Models/FineTune.php
Phase 2: Data Migration (Week 2)¶
Migrations:
2026_01_12_000001_migrate_settings_to_normalized_tables.php2026_01_12_000002_migrate_fine_tune_list_to_table.php2026_01_12_000003_populate_menu_visibility_rules.php
Helpers:
app/Services/Migration/SettingsMigrationService.phpapp/Services/Migration/MenuRulesMigrationService.php
Phase 3: Cutover (Week 3)¶
Changes:
- Replace
BladeCache::navMenu()call in navbar.blade.php - Update menu.blade.php to use NavbarService
- Create simplified blade components
- Add feature flag for gradual rollout
Phase 4: Cleanup (Week 4)¶
Removals:
- Drop deprecated settings columns
- Remove MenuService::data() (4,311 lines)
- Remove PlanHelper::planMenuCheck()
- Simplify BladeCache class
- Remove backward compatibility layer
Success Metrics¶
| Metric | Before | After Phase 4 |
|---|---|---|
navMenu calls/request |
500 | 1 |
| DB queries (navbar) | 2,681 | <10 |
| Memory (navbar render) | 386MB | <15MB |
| Render time | 7.8s | <100ms |
| MenuService.php lines | 4,311 | <200 |
| Settings migrations | 77 | 7 (new normalized) |
| Navbar blade files | 14 | 4 |
Risks & Mitigations¶
| Risk | Mitigation |
|---|---|
| Breaking existing functionality | Feature flags for gradual rollout |
| Missing visibility rules in migration | Comprehensive test coverage |
| Performance regression | A/B testing before full cutover |
| API credential encryption issues | Test with known values, verify decryption |
| Bedrock fine-tune format unknown | Design schema to be flexible, validate with AWS docs |
Open Questions¶
- Fine-tune schema: Need to verify Bedrock custom model ARN format and training job structure
- Extension detection: Should
activeExtensionsbe computed or cached? - Plan features: What's the canonical list of plan feature keys?
- Cache invalidation: How to invalidate navbar cache when menu/settings change?
Next Steps¶
- Create Phase 1 migrations
- Implement NavbarService and DTOs
- Write migration service for settings
- Test data migration in staging
- Performance benchmark before/after