Skip to content

Navbar System Rebuild Plan

Status: Planning Phase Last Updated: 2026-01-04 Decision: Option A (Normalized Tables) + DB-based visibility_rules


Table of Contents

  1. Problem Statement
  2. Current Architecture Analysis
  3. Database Redesign
  4. Menu Visibility Rules
  5. Settings Normalization
  6. Service Layer Rebuild
  7. View Layer Simplification
  8. Migration Strategy
  9. 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

  1. MenuService.php is 4,311 lines - Acts as a PHP-based database with 224+ menu definitions
  2. Per-item visibility checks - PlanHelper::planMenuCheck() called for each menu item in blade loop
  3. Circular service calls - planMenuCheck() calls MenuService::generate() which triggers more checks
  4. Settings bloat - settings_two has 40+ columns, many unused (e.g., plagiarism_key)
  5. 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)
);

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:

  1. Create a migration that reads MenuService::data() output
  2. For each item, create/update menu record with:
  3. Basic fields directly to columns
  4. active_condition + show_condition -> visibility_rules JSON
  5. Mark MenuService::data() as deprecated
  6. 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:

  1. 2026_01_05_000001_create_settings_branding_table.php
  2. 2026_01_05_000002_create_settings_ai_table.php
  3. 2026_01_05_000003_create_settings_features_table.php
  4. 2026_01_05_000004_create_settings_integrations_table.php
  5. 2026_01_05_000005_create_api_credentials_table.php
  6. 2026_01_05_000006_create_fine_tunes_table.php
  7. 2026_01_05_000007_add_visibility_rules_to_menus_table.php

New Files:

  • app/Services/Navbar/NavbarService.php
  • app/DTOs/MenuDTO.php
  • app/DTOs/UserContextDTO.php
  • app/Models/Settings/SettingsBranding.php
  • app/Models/Settings/SettingsAI.php
  • app/Models/Settings/SettingsFeatures.php
  • app/Models/Settings/SettingsIntegrations.php
  • app/Models/ApiCredential.php
  • app/Models/FineTune.php

Phase 2: Data Migration (Week 2)

Migrations:

  1. 2026_01_12_000001_migrate_settings_to_normalized_tables.php
  2. 2026_01_12_000002_migrate_fine_tune_list_to_table.php
  3. 2026_01_12_000003_populate_menu_visibility_rules.php

Helpers:

  • app/Services/Migration/SettingsMigrationService.php
  • app/Services/Migration/MenuRulesMigrationService.php

Phase 3: Cutover (Week 3)

Changes:

  1. Replace BladeCache::navMenu() call in navbar.blade.php
  2. Update menu.blade.php to use NavbarService
  3. Create simplified blade components
  4. Add feature flag for gradual rollout

Phase 4: Cleanup (Week 4)

Removals:

  1. Drop deprecated settings columns
  2. Remove MenuService::data() (4,311 lines)
  3. Remove PlanHelper::planMenuCheck()
  4. Simplify BladeCache class
  5. 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

  1. Fine-tune schema: Need to verify Bedrock custom model ARN format and training job structure
  2. Extension detection: Should activeExtensions be computed or cached?
  3. Plan features: What's the canonical list of plan feature keys?
  4. Cache invalidation: How to invalidate navbar cache when menu/settings change?

Next Steps

  1. Create Phase 1 migrations
  2. Implement NavbarService and DTOs
  3. Write migration service for settings
  4. Test data migration in staging
  5. Performance benchmark before/after