--- File: D:\projects\digitalvocano\Modules\AppManager\app\Http\Controllers\Admin\ActivationLogController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ActivationLogController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('appmanager::index');
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('appmanager::create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request) {}

    /**
     * Show the specified resource.
     */
    public function show($id)
    {
        return view('appmanager::show');
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        return view('appmanager::edit');
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id) {}

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id) {}
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Http\Controllers\Api\FileDownloadController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\DownloadableFile;
use Modules\AppManager\Entities\DownloadToken; // Import DownloadToken

class FileDownloadController extends Controller
{
    protected string $storageDisk;
    protected string $baseStoragePath = 'app_manager_files'; // Base path within the disk

    public function __construct()
    {
        $this->storageDisk = config('appmanager.storage_disk', 'local');
    }

    public function download(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'product_slug' => 'required|string|exists:am_managed_scripts,slug',
            'file_identifier' => 'required|string', // This could be the target_path or a unique ID/file_name
            'version' => 'required|string',
            'activation_token' => 'required|string', // The token received from successful activation
            'license_key' => 'sometimes|required|string', // Optional: For an extra layer of validation if token is tied to license
            'domain' => 'sometimes|required|string',      // Optional: If token is tied to a domain
        ]);

        if ($validator->fails()) {
            return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400);
        }

        // --- Robust Token Validation ---
        $downloadToken = DownloadToken::where('token', $request->input('activation_token'))->first();

        if (!$downloadToken) {
            Log::warning('AppManager: Invalid download token used.', ['token' => $request->input('activation_token'), 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Invalid or expired download token.'], 403);
        }

        if ($downloadToken->isExpired()) {
            Log::info('AppManager: Expired download token used.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Download token has expired.'], 403);
        }

        if ($downloadToken->isMaxUsesReached()) {
            Log::info('AppManager: Download token max uses reached.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Download token usage limit reached.'], 403);
        }

        // Optional: Further checks if token is tied to specific license_key or domain
        // This requires the DownloadToken to have a direct relationship or stored metadata
        // For example, if DownloadToken belongs to a License:
        if ($request->filled('license_key') && $downloadToken->license->license_key !== $request->input('license_key')) {
             Log::warning('AppManager: Download token license mismatch.', [
                'token_id' => $downloadToken->id,
                'expected_key' => $downloadToken->license->license_key,
                'provided_key' => $request->input('license_key'),
                'ip' => $request->ip()
            ]);
            return response()->json(['status' => 'error', 'message' => 'Token not valid for this license.'], 403);
        }
        // You might also check if the token was generated for the correct product_slug via $downloadToken->license->managed_script_id

        // --- End Robust Token Validation ---


        $script = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail();
        $downloadableFile = DownloadableFile::where('managed_script_id', $script->id)
                                            ->where(function($query) use ($request) {
                                                // Allow lookup by target_path primarily, or file_name as a fallback
                                                $query->where('target_path', $request->input('file_identifier'))
                                                      ->orWhere('file_name', $request->input('file_identifier'));
                                            })
                                            ->where('version', $request->input('version'))
                                            ->first();

        if (!$downloadableFile) {
            Log::info('AppManager: Downloadable file not found or version mismatch.', $request->all());
            return response()->json(['status' => 'error', 'message' => 'File not found or version mismatch.'], 404);
        }

        $filePathOnDisk = $downloadableFile->file_path; // This path is relative to the disk's root

        if (!Storage::disk($this->storageDisk)->exists($filePathOnDisk)) {
            Log::error("AppManager: File source not found on server for download.", ['path' => $filePathOnDisk, 'request' => $request->all()]);
            return response()->json(['status' => 'error', 'message' => 'File source not found on server.'], 500);
        }

        // Optional: Verify hash before sending
        $currentHash = hash_file('sha256', Storage::disk($this->storageDisk)->path($filePathOnDisk));
        if ($downloadableFile->file_hash && $currentHash !== $downloadableFile->file_hash) {
            Log::critical("AppManager: CRITICAL - File hash mismatch for download.", [
                'file_record_id' => $downloadableFile->id,
                'path' => $filePathOnDisk,
                'stored_hash' => $downloadableFile->file_hash,
                'current_hash' => $currentHash
            ]);
            return response()->json(['status' => 'error', 'message' => 'File integrity check failed on server.'], 500);
        }
        
        $downloadToken->incrementUsage();
        // Optionally, delete the token if it has reached max uses and you want it to be strictly single-session for a set of downloads
        // if ($downloadToken->isMaxUsesReached()) { $downloadToken->delete(); }

        return Storage::disk($this->storageDisk)->download($filePathOnDisk, $downloadableFile->file_name, [
            'Content-Type' => Storage::disk($this->storageDisk)->mimeType($filePathOnDisk) ?? 'application/octet-stream',
            'X-File-Hash' => $downloadableFile->file_hash // Send hash for client-side verification
        ]);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Http\Controllers\AppManagerController.php ---
<?php

namespace Modules\AppManager\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class AppManagerController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('appmanager::index');
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('appmanager::create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request) {}

    /**
     * Show the specified resource.
     */
    public function show($id)
    {
        return view('appmanager::show');
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        return view('appmanager::edit');
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id) {}

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id) {}
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\ActivationLog.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ActivationLog extends Model
{
    use HasFactory;

    protected $table = 'am_activation_logs';

    public $timestamps = false; // We only have 'activated_at'

    protected $fillable = [
        'license_id',
        'activated_domain',
        'ip_address',
        'server_signature',
        'status',
        'message',
        'activated_at',
    ];

    protected $casts = [
        'activated_at' => 'datetime',
    ];

    public function license(): BelongsTo
    {
        return $this->belongsTo(License::class);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\DownloadableFile.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class DownloadableFile extends Model
{
    use HasFactory;

    protected $table = 'am_downloadable_files';

    protected $fillable = [
        'managed_script_id',
        'file_name',
        'file_path',
        'version',
        'target_path',
        'file_hash',
        'description',
        'is_critical',
    ];

    protected static function newFactory()
    {
        // return \Modules\AppManager\Database\factories\DownloadableFileFactory::new();
    }

    public function managedScript(): BelongsTo
    {
        return $this->belongsTo(ManagedScript::class);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\DownloadToken.php ---
<?php

namespace Modules\AppManager\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
// use Modules\AppManager\Database\Factories\DownloadTokenFactory;

class DownloadToken extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     */
    protected $fillable = [];

    // protected static function newFactory(): DownloadTokenFactory
    // {
    //     // return DownloadTokenFactory::new();
    // }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\License.php ---
<?php

namespace Modules\AppManager\Entities; // Corrected namespace based on other AppManager models

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;

class License extends Model
{
    use HasFactory;

    protected $table = 'am_licenses';

    protected $fillable = [
        'managed_script_id',
        'license_key',
        'type',
        'customer_email',
        'customer_name',
        'purchase_code',
        'activation_limit',
        'current_activations',
        'status',
        'expires_at',
        'supported_until',
        'metadata',
        'notes', // Added notes
    ];

    protected $casts = [
        'expires_at' => 'datetime',
        'supported_until' => 'datetime',
        'metadata' => 'array',
        // 'is_boilerplate_core_license' => 'boolean', // Example if you have such a field, ensure it's in fillable if mass assignable
    ];

    // If you have a factory, uncomment and point to the correct path
    // protected static function newFactory()
    // {
    //     return \Modules\AppManager\Database\factories\LicenseFactory::new();
    // }

    public function managedScript(): BelongsTo
    {
        return $this->belongsTo(ManagedScript::class);
    }

    public function activationLogs(): HasMany
    {
        // Assuming ActivationLog model is in the same namespace
        return $this->hasMany(ActivationLog::class);
    }

    protected static function booted()
    {
        parent::boot(); // It's good practice to call parent::boot()
    
        static::creating(function ($license) {
            // Check for the temporary flag set by the controller
            if (empty($license->license_key) && ($license->attributes['auto_generate_key_flag'] ?? false)) {
                $maxAttempts = 5; // Prevent infinite loop in rare collision cases
                $attempt = 0;
                do {
                    // Generate a more robust key
                    // Example: LIC-SCRIPTNAME-RANDOM-RANDOM-RANDOM-RANDOM
                    $scriptPrefix = strtoupper(Str::slug($license->managedScript->name ?? 'APP', ''));
                    $prefix = "LIC-{$scriptPrefix}-";
                    $key = $prefix .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6));
                    $attempt++;
                } while (static::where('license_key', $key)->exists() && $attempt < $maxAttempts);

                if ($attempt >= $maxAttempts && static::where('license_key', $key)->exists()) {
                    // Fallback or throw an exception if a unique key couldn't be generated
                    // For simplicity, appending a timestamp or more random chars
                    $key .= '-' . strtoupper(Str::random(4));
                }
                $license->license_key = $key;
            }
            // Remove the temporary flag as it's not a DB column
            unset($license->attributes['auto_generate_key_flag']);
        });

        static::updating(function ($license) {
            // Check if the license_key is being cleared/empty and auto_generate_key_flag is set
            if (empty($license->license_key) && ($license->attributes['auto_generate_key_flag'] ?? false)) {
                $maxAttempts = 5;
                $attempt = 0;
                do {
                    $scriptPrefix = strtoupper(Str::slug($license->managedScript->name ?? 'APP', ''));
                    $prefix = "LIC-{$scriptPrefix}-";
                    $key = $prefix .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6)) . '-' .
                           strtoupper(Str::random(6));
                    $attempt++;
                } while (static::where('license_key', $key)->where('id', '!=', $license->id)->exists() && $attempt < $maxAttempts);

                if ($attempt >= $maxAttempts && static::where('license_key', $key)->where('id', '!=', $license->id)->exists()) {
                    $key .= '-' . strtoupper(Str::random(4));
                }
                $license->license_key = $key;
            }
            // Remove the temporary flag as it's not a DB column
            unset($license->attributes['auto_generate_key_flag']);
        });
    }

    /**
     * Get all unique successfully activated domains for this license.
     *
     * @return \Illuminate\Support\Collection
     */
    public function getActiveDomains(): \Illuminate\Support\Collection
    {
        return $this->activationLogs()
                    ->where('status', 'success') // Assuming 'success' is the status for successful activation
                    ->distinct('activated_domain')
                    ->orderBy('activated_at', 'desc') // Optional: get the most recent first
                    ->pluck('activated_domain');
    }

    /**
     * Increment the current activations count.
     *
     * @return bool
     */
    public function incrementActivations(): bool
    {
        $this->current_activations = ($this->current_activations ?? 0) + 1;
        return $this->save();
    }

    /**
     * Decrement the current activations count.
     *
     * @return bool
     */
    public function decrementActivations(): bool
    {
        if ($this->current_activations > 0) {
            $this->current_activations--;
            return $this->save();
        }
        return true; // Or false if you want to indicate no change was made
    }

    /**
     * Check if the license has reached its activation limit.
     * Considers 0 as unlimited.
     *
     * @return bool
     */
    public function hasReachedActivationLimit(): bool
    {
        if ($this->activation_limit == 0) { // 0 means unlimited
            return false;
        }
        return ($this->current_activations ?? 0) >= $this->activation_limit;
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\ManagedScript.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;

class ManagedScript extends Model
{
    use HasFactory;

    protected $table = 'am_managed_scripts';

    protected $fillable = [
        'name',
        'slug',
        'current_version',
        'description',
        'changelog',
        'is_boilerplate_core',
        'status',
        'envato_item_id', // Add this
    ];

    protected $casts = [
        'is_boilerplate_core' => 'boolean',
    ];

    protected static function newFactory()
    {
        // return \Modules\AppManager\Database\factories\ManagedScriptFactory::new();
    }

    public function downloadableFiles(): HasMany
    {
        return $this->hasMany(DownloadableFile::class);
    }

    public function licenses(): HasMany
    {
        return $this->hasMany(License::class);
    }

    protected static function booted()
    {
        static::creating(function ($script) {
            if (empty($script->slug)) {
                $script->slug = Str::slug($script->name);
            }
        });
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\AppManagerServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Nwidart\Modules\Traits\PathNamespace;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class AppManagerServiceProvider extends ServiceProvider
{
    use PathNamespace;

    protected string $name = 'AppManager';

    protected string $nameLower = 'appmanager';

    /**
     * Boot the application events.
     */
    public function boot(): void
    {
        $this->registerCommands();
        $this->registerCommandSchedules();
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->loadMigrationsFrom(module_path($this->name, 'database/migrations'));
    }

    /**
     * Register the service provider.
     */
    public function register(): void
    {
        $this->app->register(EventServiceProvider::class);
        $this->app->register(RouteServiceProvider::class);
    }

    /**
     * Register commands in the format of Command::class
     */
    protected function registerCommands(): void
    {
        // $this->commands([]);
    }

    /**
     * Register command Schedules.
     */
    protected function registerCommandSchedules(): void
    {
        // $this->app->booted(function () {
        //     $schedule = $this->app->make(Schedule::class);
        //     $schedule->command('inspire')->hourly();
        // });
    }

    /**
     * Register translations.
     */
    public function registerTranslations(): void
    {
        $langPath = resource_path('lang/modules/'.$this->nameLower);

        if (is_dir($langPath)) {
            $this->loadTranslationsFrom($langPath, $this->nameLower);
            $this->loadJsonTranslationsFrom($langPath);
        } else {
            $this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower);
            $this->loadJsonTranslationsFrom(module_path($this->name, 'lang'));
        }
    }

    /**
     * Register config.
     */
    protected function registerConfig(): void
    {
        $configPath = module_path($this->name, config('modules.paths.generator.config.path'));

        if (is_dir($configPath)) {
            $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));

            foreach ($iterator as $file) {
                if ($file->isFile() && $file->getExtension() === 'php') {
                    $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
                    $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
                    $segments = explode('.', $this->nameLower.'.'.$config_key);

                    // Remove duplicated adjacent segments
                    $normalized = [];
                    foreach ($segments as $segment) {
                        if (end($normalized) !== $segment) {
                            $normalized[] = $segment;
                        }
                    }

                    $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);

                    $this->publishes([$file->getPathname() => config_path($config)], 'config');
                    $this->merge_config_from($file->getPathname(), $key);
                }
            }
        }
    }

    /**
     * Merge config from the given path recursively.
     */
    protected function merge_config_from(string $path, string $key): void
    {
        $existing = config($key, []);
        $module_config = require $path;

        config([$key => array_replace_recursive($existing, $module_config)]);
    }

    /**
     * Register views.
     */
    public function registerViews(): void
    {
        $viewPath = resource_path('views/modules/'.$this->nameLower);
        $sourcePath = module_path($this->name, 'resources/views');

        $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);

        Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower);
    }

    /**
     * Get the services provided by the provider.
     */
    public function provides(): array
    {
        return [];
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (config('view.paths') as $path) {
            if (is_dir($path.'/modules/'.$this->nameLower)) {
                $paths[] = $path.'/modules/'.$this->nameLower;
            }
        }

        return $paths;
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\EventServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event handler mappings for the application.
     *
     * @var array<string, array<int, string>>
     */
    protected $listen = [];

    /**
     * Indicates if events should be discovered.
     *
     * @var bool
     */
    protected static $shouldDiscoverEvents = true;

    /**
     * Configure the proper event listeners for email verification.
     */
    protected function configureEmailVerification(): void {}
}


--- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\RouteServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    protected string $name = 'AppManager';

    /**
     * Called before routes are registered.
     *
     * Register any model bindings or pattern based filters.
     */
    public function boot(): void
    {
        parent::boot();
    }

    /**
     * Define the routes for the application.
     */
    public function map(): void
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application.
     *
     * These routes all receive session state, CSRF protection, etc.
     */
    protected function mapWebRoutes(): void
    {
        Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
    }

    /**
     * Define the "api" routes for the application.
     *
     * These routes are typically stateless.
     */
    protected function mapApiRoutes(): void
    {
        Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php'));
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\config\config.php ---
<?php

return [
    'name' => 'AppManager',

    /*
    |--------------------------------------------------------------------------
    | Storage Disk for Downloadable Files
    |--------------------------------------------------------------------------
    |
    | Specify the Laravel filesystem disk where downloadable files managed by
    | AppManager will be stored. 'local' is recommended for security,
    | as files will not be publicly accessible via a URL.
    |
    */
    'storage_disk' => env('APPMANAGER_STORAGE_DISK', 'local'),

    /*
    |--------------------------------------------------------------------------
    | Envato API Configuration
    |--------------------------------------------------------------------------
    |
    | Settings for integrating with the Envato API to verify purchase codes.
    |
    */
    'envato_personal_token' => env('ENVATO_PERSONAL_TOKEN', null),
    // You might map product slugs to Envato Item IDs here or in the database
    // 'envato_item_ids' => [
    //     'your-script-slug' => 12345678, // Example Envato Item ID
    // ],

    /*
    |--------------------------------------------------------------------------
    | Download Token Configuration
    |--------------------------------------------------------------------------
    */
    'download_token_ttl_minutes' => env('APPMANAGER_DOWNLOAD_TOKEN_TTL', 30), // Time-to-live in minutes
    'download_token_max_uses' => env('APPMANAGER_DOWNLOAD_TOKEN_MAX_USES', 5), // Max uses per token (e.g., for a set of files)
];


--- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\ActivationLogFactoryFactory.php ---
<?php

namespace Modules\AppManager\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ActivationLogFactoryFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     */
    protected $model = \Modules\AppManager\Models\ActivationLogFactory::class;

    /**
     * Define the model's default state.
     */
    public function definition(): array
    {
        return [];
    }
}



--- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\DownloadTokenFactory.php ---
<?php

namespace Modules\AppManager\Database\factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Modules\AppManager\Entities\License;

class DownloadTokenFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = \Modules\AppManager\Entities\DownloadToken::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'license_id' => License::factory(), // Assumes LicenseFactory exists
            'token' => Str::random(60),
            'ip_address' => $this->faker->ipv4,
            'user_agent' => $this->faker->userAgent,
            'uses' => 0,
            'max_uses' => $this->faker->numberBetween(1, 5),
            'expires_at' => now()->addHours($this->faker->numberBetween(1, 24)),
        ];
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\DownloadTokenFactoryFactory.php ---
<?php

namespace Modules\AppManager\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class DownloadTokenFactoryFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     */
    protected $model = \Modules\AppManager\Models\DownloadTokenFactory::class;

    /**
     * Define the model's default state.
     */
    public function definition(): array
    {
        return [];
    }
}



--- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\LicenseFactoryFactory.php ---
<?php

namespace Modules\AppManager\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class LicenseFactoryFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     */
    protected $model = \Modules\AppManager\Models\LicenseFactory::class;

    /**
     * Define the model's default state.
     */
    public function definition(): array
    {
        return [];
    }
}



--- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\ManagedScriptFactoryFactory.php ---
<?php

namespace Modules\AppManager\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ManagedScriptFactoryFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     */
    protected $model = \Modules\AppManager\Models\ManagedScriptFactory::class;

    /**
     * Define the model's default state.
     */
    public function definition(): array
    {
        return [];
    }
}



--- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_120151_create_downloadable_files_table.php ---
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('downloadable_files', function (Blueprint $table) {
            $table->id();
            
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('downloadable_files');
    }
};


--- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_120648_create_licenses_table.php ---
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('licenses', function (Blueprint $table) {
            $table->id();
            
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('licenses');
    }
};


--- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_120741_create_activation_logs_table.php ---
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('activation_logs', function (Blueprint $table) {
            $table->id();
            
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('activation_logs');
    }
};


--- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_124924_add_envato_item_id_to_am_managed_scripts_table.php ---
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('am_managed_scripts', function (Blueprint $table) {
            
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('am_managed_scripts', function (Blueprint $table) {
            
        });
    }
};


--- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_125242_create_download_tokens_table.php ---
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('download_tokens', function (Blueprint $table) {
            $table->id();
            
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('download_tokens');
    }
};


--- File: D:\projects\digitalvocano\Modules\AppManager\database\seeders\AppManagerDatabaseSeeder.php ---
<?php

namespace Modules\AppManager\Database\Seeders;

use Illuminate\Database\Seeder;

class AppManagerDatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // $this->call([]);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Entities\ActivationLog.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ActivationLog extends Model
{
    use HasFactory;

    protected $table = 'am_activation_logs';

    public $timestamps = false; // We only have 'activated_at'

    protected $fillable = [
        'license_id',
        'managed_script_id', // Added: To link directly to the script being activated
        'activated_domain',
        'ip_address',
        'server_signature',
        'status',
        'message',
        'activated_at',
        'license_key_attempt', // Added: To log the license key that was attempted
        'purchase_code_attempt', // Added: To log the purchase code that was attempted
    ];

    protected $casts = [
        'activated_at' => 'datetime',
    ];

    public function license(): BelongsTo
    {
        return $this->belongsTo(License::class);
    }

    /**
     * Get the managed script associated with the activation log.
     */
    public function managedScript(): BelongsTo
    {
        return $this->belongsTo(ManagedScript::class);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Entities\DownloadableFile.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class DownloadableFile extends Model
{
    use HasFactory;

    protected $table = 'am_downloadable_files';

    protected $fillable = [
        'managed_script_id',
        'file_name',
        'file_path',
        'version',
        'target_path',
        'file_hash',
        'description',
        'is_critical',
    ];

    protected static function newFactory()
    {
        // return \Modules\AppManager\Database\factories\DownloadableFileFactory::new();
    }

    public function managedScript(): BelongsTo
    {
        return $this->belongsTo(ManagedScript::class);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Entities\DownloadToken.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;

class DownloadToken extends Model
{
    use HasFactory;

    protected $table = 'am_download_tokens';

    protected $fillable = [
        'license_id',
        'token',
        'ip_address',
        'user_agent',
        'uses',
        'max_uses',
        'expires_at',
    ];

    protected $casts = [
        'expires_at' => 'datetime',
    ];

    protected static function newFactory()
    {
        return \Modules\AppManager\Database\factories\DownloadTokenFactory::new();
    }

    public function license(): BelongsTo
    {
        return $this->belongsTo(License::class);
    }

    protected static function booted()
    {
        static::creating(function ($token) {
            if (empty($token->token)) {
                $token->token = Str::random(60); // Ensure token is generated
            }
        });
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Entities\License.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;

class License extends Model
{
    use HasFactory;

    protected $table = 'am_licenses';

    protected $fillable = [
        'managed_script_id',
        'license_key',
        'type',
        'customer_email',
        'customer_name', // This was already here, good.
        'purchase_code',
        'activation_limit',
        'current_activations',
        'status',
        'expires_at',
        'supported_until',
        'metadata',
    ];

    protected $casts = [
        'expires_at' => 'datetime',
        'supported_until' => 'datetime',
        'metadata' => 'array',
    ];

    protected static function newFactory()
    {
        // return \Modules\AppManager\Database\factories\LicenseFactory::new();
    }

    public function managedScript(): BelongsTo
    {
        return $this->belongsTo(ManagedScript::class);
    }

    public function activationLogs(): HasMany
    {
        return $this->hasMany(ActivationLog::class);
    }

    protected static function booted()
    {
        static::creating(function ($license) {
            if (empty($license->license_key)) {
                // Generate a more robust key, e.g., prefix + UUID
                $prefix = strtoupper(Str::slug($license->managedScript->name ?? 'LIC', '_'));
                $license->license_key = $prefix . '_' . Str::uuid()->toString();
            }
        });
    }

    public function getActiveDomains(): \Illuminate\Support\Collection
    {
        return $this->activationLogs()
                    ->where('status', 'success')
                    ->distinct('activated_domain') // Ensure we only get unique domains
                    ->pluck('activated_domain');
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Entities\ManagedScript.php ---
<?php

namespace Modules\AppManager\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;

class ManagedScript extends Model
{
    use HasFactory;

    protected $table = 'am_managed_scripts';

    protected $fillable = [
        'name',
        'slug',
        'current_version',
        'description',
        'changelog',
        'is_boilerplate_core',
        'status',
        'envato_item_id', // Add this
    ];

    protected $casts = [
        'is_boilerplate_core' => 'boolean',
    ];

    protected static function newFactory()
    {
        // return \Modules\AppManager\Database\factories\ManagedScriptFactory::new();
    }

    public function downloadableFiles(): HasMany
    {
        return $this->hasMany(DownloadableFile::class);
    }

    public function licenses(): HasMany
    {
        return $this->hasMany(License::class);
    }

    protected static function booted()
    {
        static::creating(function ($script) {
            if (empty($script->slug)) {
                $script->slug = Str::slug($script->name);
            }
        });
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\ActivationLogController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\AppManager\Entities\ActivationLog;

class ActivationLogController extends Controller
{
    public function index(Request $request)
    {
        $query = ActivationLog::with(['license', 'license.managedScript'])->orderBy('activated_at', 'desc');

        if ($request->filled('search')) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('activated_domain', 'like', "%{$searchTerm}%")
                  ->orWhere('ip_address', 'like', "%{$searchTerm}%")
                  ->orWhereHas('license', function ($lq) use ($searchTerm) {
                      $lq->where('license_key', 'like', "%{$searchTerm}%")
                         ->orWhere('customer_email', 'like', "%{$searchTerm}%");
                  });
            });
        }
         if ($request->filled('status') && $request->status !== 'all') {
            $query->where('status', $request->input('status'));
        }

        $logs = $query->paginate(20)->withQueryString();
        $statuses = ['success' => 'Success', 'failed_invalid_key' => 'Invalid Key', 'failed_limit_reached' => 'Limit Reached', 'failed_expired' => 'Expired', 'failed_domain_mismatch' => 'Domain Mismatch', 'failed_server_error' => 'Server Error'];


        return view('appmanager::admin.activation_logs.index', compact('logs', 'statuses'));
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\DownloadableFileController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\DownloadableFile;

class DownloadableFileController extends Controller
{
    protected string $storageDisk = 'local'; // Use 'local' for storage/app, or 'public' if files need web access (less secure for this use case)
    protected string $baseStoragePath = 'app_manager_files';

    /**
     * Display a listing of the resource.
     * @param ManagedScript $script
     * @return \Illuminate\Contracts\View\View
     */
    public function index(ManagedScript $script)
    {
        $files = $script->downloadableFiles()->orderBy('version', 'desc')->orderBy('file_name')->paginate(15);
        return view('appmanager::admin.downloadable_files.index', compact('script', 'files'));
    }

    /**
     * Show the form for creating a new resource.
     * @param ManagedScript $script
     * @return \Illuminate\Contracts\View\View
     */
    public function create(ManagedScript $script)
    {
        return view('appmanager::admin.downloadable_files.create', compact('script'));
    }

    /**
     * Store a newly created resource in storage.
     * @param Request $request
     * @param ManagedScript $script
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(Request $request, ManagedScript $script)
    {
        $request->validate([
            'uploaded_file' => 'required|file|max:102400', // Max 100MB, adjust as needed
            'version' => 'required|string|max:50',
            'target_path' => 'required|string|max:255',
            'description' => 'nullable|string',
            'is_critical' => 'boolean',
        ]);

        $data = $request->only(['version', 'target_path', 'description']);
        $data['is_critical'] = $request->has('is_critical');

        if ($request->hasFile('uploaded_file')) {
            $file = $request->file('uploaded_file');
            $originalFileName = $file->getClientOriginalName();
            // Sanitize original file name for storage path, or use a UUID
            $safeFileName = Str::slug(pathinfo($originalFileName, PATHINFO_FILENAME)) . '.' . $file->getClientOriginalExtension();

            $versionPath = Str::slug($data['version']);
            $securePath = "{$this->baseStoragePath}/{$script->slug}/{$versionPath}";

            // Store the file
            $storedFilePath = $file->storeAs($securePath, $safeFileName, $this->storageDisk);

            if (!$storedFilePath) {
                return redirect()->back()->with('error', 'Failed to upload file. Please check storage permissions.')->withInput();
            }

            $data['file_name'] = $originalFileName;
            $data['file_path'] = $storedFilePath; // Path relative to the disk's root
            $data['file_hash'] = hash_file('sha256', Storage::disk($this->storageDisk)->path($storedFilePath));
        } else {
            return redirect()->back()->with('error', 'File is required.')->withInput();
        }

        $script->downloadableFiles()->create($data);

        return redirect()->route('admin.appmanager.scripts.files.index', $script)
                         ->with('success', 'Downloadable file uploaded and created successfully.');
    }

    /**
     * Show the form for editing the specified resource.
     * @param ManagedScript $script
     * @param DownloadableFile $file
     * @return \Illuminate\Contracts\View\View
     */
    public function edit(ManagedScript $script, DownloadableFile $file)
    {
        if ($file->managed_script_id !== $script->id) {
            abort(404); // Or redirect with error
        }
        return view('appmanager::admin.downloadable_files.edit', compact('script', 'file'));
    }

    /**
     * Update the specified resource in storage.
     * @param Request $request
     * @param ManagedScript $script
     * @param DownloadableFile $file
     * @return \Illuminate\Http\RedirectResponse
     */
    public function update(Request $request, ManagedScript $script, DownloadableFile $file)
    {
        if ($file->managed_script_id !== $script->id) {
            abort(404);
        }

        $request->validate([
            'uploaded_file' => 'nullable|file|max:102400', // Optional on update
            'version' => 'required|string|max:50',
            'target_path' => 'required|string|max:255',
            'description' => 'nullable|string',
            'is_critical' => 'boolean',
        ]);

        $data = $request->only(['version', 'target_path', 'description']);
        $data['is_critical'] = $request->has('is_critical');

        if ($request->hasFile('uploaded_file')) {
            // Delete old file
            if ($file->file_path && Storage::disk($this->storageDisk)->exists($file->file_path)) {
                Storage::disk($this->storageDisk)->delete($file->file_path);
            }

            $newUploadedFile = $request->file('uploaded_file');
            $originalFileName = $newUploadedFile->getClientOriginalName();
            $safeFileName = Str::slug(pathinfo($originalFileName, PATHINFO_FILENAME)) . '.' . $newUploadedFile->getClientOriginalExtension();
            $versionPath = Str::slug($data['version']);
            $securePath = "{$this->baseStoragePath}/{$script->slug}/{$versionPath}";

            $storedFilePath = $newUploadedFile->storeAs($securePath, $safeFileName, $this->storageDisk);

            if (!$storedFilePath) {
                return redirect()->back()->with('error', 'Failed to upload new file. Please check storage permissions.')->withInput();
            }

            $data['file_name'] = $originalFileName;
            $data['file_path'] = $storedFilePath;
            $data['file_hash'] = hash_file('sha256', Storage::disk($this->storageDisk)->path($storedFilePath));
        }

        $file->update($data);

        return redirect()->route('admin.appmanager.scripts.files.index', $script)
                         ->with('success', 'Downloadable file updated successfully.');
    }

    /**
     * Remove the specified resource from storage.
     * @param ManagedScript $script
     * @param DownloadableFile $file
     * @return \Illuminate\Http\RedirectResponse
     */
    public function destroy(ManagedScript $script, DownloadableFile $file)
    {
        if ($file->managed_script_id !== $script->id) {
            abort(404);
        }

        try {
            if ($file->file_path && Storage::disk($this->storageDisk)->exists($file->file_path)) {
                Storage::disk($this->storageDisk)->delete($file->file_path);
            }
            $file->delete();
            return redirect()->route('admin.appmanager.scripts.files.index', $script)
                             ->with('success', 'Downloadable file deleted successfully.');
        } catch (\Exception $e) {
            return redirect()->route('admin.appmanager.scripts.files.index', $script)
                             ->with('error', 'Failed to delete file: ' . $e->getMessage());
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\LicenseController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\AppManager\Entities\License;
use Modules\AppManager\Entities\ManagedScript;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;

class LicenseController extends Controller
{
    public function index(Request $request)
    {
        $query = License::with('managedScript')->orderBy('created_at', 'desc');

        if ($request->filled('search')) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('license_key', 'like', "%{$searchTerm}%")
                  ->orWhere('customer_email', 'like', "%{$searchTerm}%")
                  ->orWhere('purchase_code', 'like', "%{$searchTerm}%")
                  ->orWhereHas('managedScript', function ($sq) use ($searchTerm) {
                      $sq->where('name', 'like', "%{$searchTerm}%");
                  });
            });
        }
        if ($request->filled('managed_script_id')) {
            $query->where('managed_script_id', $request->input('managed_script_id'));
        }
        if ($request->filled('status') && $request->status !== 'all') {
            $query->where('status', $request->input('status'));
        }

        $licenses = $query->paginate(15)->withQueryString();
        $scripts = ManagedScript::orderBy('name')->pluck('name', 'id');
        $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked'];

        return view('appmanager::admin.licenses.index', compact('licenses', 'scripts', 'statuses'));
    }

    public function create()
    {
        $scripts = ManagedScript::orderBy('name')->pluck('name', 'id');
        $types = ['internal' => 'Internal', 'codecanyon' => 'CodeCanyon', 'subscription' => 'Subscription'];
        $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked'];
        return view('appmanager::admin.licenses.create', compact('scripts', 'types', 'statuses'));
    }

    public function store(Request $request)
    {
        $rules = [
            'managed_script_id' => 'required|exists:am_managed_scripts,id',
            'auto_generate_key' => 'nullable|boolean',
            'type' => 'required|in:internal,codecanyon,subscription',
            'customer_email' => 'nullable|email|max:255',
            'customer_name' => 'nullable|string|max:255',
            'purchase_code' => [
                'nullable', 'string', 'max:255',
                Rule::unique('am_licenses')->where(function ($query) use ($request) {
                    return $query->where('type', 'codecanyon'); // Ensures uniqueness among CodeCanyon type licenses if purchase_code is provided
                }),
            ],
            'activation_limit' => 'required|integer|min:0', // 0 for unlimited
            'status' => 'required|in:pending,active,suspended,expired,revoked',
            'expires_at' => 'nullable|date',
            'does_not_expire' => 'nullable|boolean',
            'supported_until' => 'nullable|date',
            'metadata' => 'nullable|json',
            'notes' => 'nullable|string',
        ];
        
        // Conditionally add rules for license_key
        if ($request->boolean('auto_generate_key')) {
            // If auto-generating, license_key is optional from input. If provided, validate it.
            $rules['license_key'] = [
                'nullable',
                'string',
                'max:255',
                Rule::unique('am_licenses', 'license_key'), // Corrected: unique rule on nullable field handles "unique if filled"
            ];
        } else {
            // If not auto-generating, license_key is required and must be unique.
            $rules['license_key'] = [
                'required',
                'string',
                'max:255',
                Rule::unique('am_licenses', 'license_key'),
            ];
        }
        $request->validate($rules);

        $data = $request->except(['auto_generate_key', 'does_not_expire']);

        if ($request->boolean('auto_generate_key') && empty($request->input('license_key'))) {
            $data['auto_generate_key_flag'] = true; // Model will handle generation
        }

        if ($request->boolean('does_not_expire')) {
            $data['expires_at'] = null;
        }

        if ($data['type'] !== 'codecanyon') {
            $data['purchase_code'] = null; // Ensure purchase code is null if not CodeCanyon type
        }


        License::create($data);

        return redirect()->route('admin.appmanager.licenses.index')
                         ->with('success', 'License created successfully.');
    }

        public function show(License $license)
        {
            $license->load(['managedScript', 'activationLogs' => function ($query) {
                $query->orderBy('activated_at', 'desc');
            }]);
            return view('appmanager::admin.licenses.show', compact('license'));
        }
    public function edit(License $license)
    {
        $scripts = ManagedScript::orderBy('name')->pluck('name', 'id');
        $types = ['internal' => 'Internal', 'codecanyon' => 'CodeCanyon', 'subscription' => 'Subscription'];
        $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked'];
        return view('appmanager::admin.licenses.edit', compact('license', 'scripts', 'types', 'statuses'));
    }

    public function update(Request $request, License $license)
    {
        $rules = [
            'managed_script_id' => 'required|exists:am_managed_scripts,id',
            'type' => 'required|in:internal,codecanyon,subscription',
            'customer_email' => 'nullable|email|max:255',
            'customer_name' => 'nullable|string|max:255',
            'purchase_code' => [
                'nullable', 'string', 'max:255',
                Rule::unique('am_licenses')->where(function ($query) use ($request) {
                    return $query->where('type', 'codecanyon');
                })->ignore($license->id),
            ],
            'activation_limit' => 'required|integer|min:0',
            'status' => 'required|in:pending,active,suspended,expired,revoked',
            'expires_at' => 'nullable|date',
            'does_not_expire' => 'nullable|boolean',
            'supported_until' => 'nullable|date',
            'metadata' => 'nullable|json',
            'notes' => 'nullable|string',
        ];
        
        // Conditionally add rules for license_key for update
        if ($request->boolean('auto_generate_key')) {
            // If user checks auto_generate_key on update (perhaps after clearing the key)
            $rules['license_key'] = [
                'nullable',
                'string',
                'max:255',
                Rule::unique('am_licenses', 'license_key')->ignore($license->id), // Corrected: unique rule on nullable field handles "unique if filled"
            ];
        } else {
            $rules['license_key'] = [
                'required',
                'string',
                'max:255',
                Rule::unique('am_licenses', 'license_key')->ignore($license->id),
            ];
        }
        $request->validate($rules);

        $data = $request->except(['auto_generate_key', 'does_not_expire']);

        if ($request->boolean('does_not_expire')) {
            $data['expires_at'] = null;
        }

        // If user cleared the key and wants to auto-generate a new one on update
        if ($request->boolean('auto_generate_key') && empty($request->input('license_key'))) {
            $data['auto_generate_key_flag'] = true; // Model's 'updating' hook will handle generation
        }

        if ($data['type'] !== 'codecanyon') {
            $data['purchase_code'] = null;
        }

        $license->update($data);

        return redirect()->route('admin.appmanager.licenses.index')
                         ->with('success', 'License updated successfully.');
    }

    public function destroy(License $license)
    {
        $license->delete();
        return redirect()->route('admin.appmanager.licenses.index')
                         ->with('success', 'License deleted successfully.');
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\ManagedScriptController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\License;
use Modules\AppManager\Entities\ActivationLog;
use Illuminate\Support\Str;

class ManagedScriptController extends Controller
{
    /**
     * Display a listing of the resource.
     * @return \Illuminate\Contracts\View\View
     */
    public function index(Request $request)
    {
        $query = ManagedScript::withCount('licenses')->orderBy('name'); // Eager load license count
        if ($request->filled('search')) {
            $searchTerm = $request->input('search');
            $query->where(function ($q) use ($searchTerm) {
                $q->where('name', 'like', "%{$searchTerm}%")
                  ->orWhere('slug', 'like', "%{$searchTerm}%")
                  ->orWhere('description', 'like', "%{$searchTerm}%");
            });
        }
        $scripts = $query->paginate(15)->withQueryString();

        // New Stats Data
        $totalScriptsCount = ManagedScript::count();
        $totalLicensesCount = License::count();
        $recentSuccessfulActivationsCount = ActivationLog::where('status', 'success')
                                               ->where('activated_at', '>=', now()->subDays(7))
                                               ->count();

        return view('appmanager::admin.managed_scripts.index', compact(
            'scripts',
            'totalScriptsCount',
            'totalLicensesCount',
            'recentSuccessfulActivationsCount'
        ));
    }

    /**
     * Show the form for creating a new resource.
     * @return \Illuminate\Contracts\View\View
     */
    public function create()
    {
        $statuses = ['development' => 'Development', 'active' => 'Active', 'beta' => 'Beta', 'deprecated' => 'Deprecated', 'archived' => 'Archived'];
        return view('appmanager::admin.managed_scripts.create', compact('statuses'));
    }

    /**
     * Store a newly created resource in storage.
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255|unique:am_managed_scripts,name',
            'slug' => 'nullable|string|max:255|unique:am_managed_scripts,slug',
            'current_version' => 'required|string|max:50',
            'description' => 'nullable|string',
            'changelog' => 'nullable|string',
            'is_boilerplate_core' => 'boolean',
            'status' => 'required|in:development,active,beta,deprecated,archived',
            'envato_item_id' => 'nullable|string|max:50', // Add validation
        ]);

        $data = $request->all();
        $data['slug'] = $request->input('slug') ?: Str::slug($request->input('name'));
        $data['is_boilerplate_core'] = $request->has('is_boilerplate_core');

        ManagedScript::create($data);

        return redirect()->route('admin.appmanager.scripts.index')
                         ->with('success', 'Managed script created successfully.');
    }

    /**
     * Display the specified resource.
     * @param ManagedScript $script
     * @return \Illuminate\Contracts\View\View
     */
    public function show(ManagedScript $script)
    {
        // Typically, for admin, edit is more common than show.
        // You might want to show associated files or licenses here.
        return view('appmanager::admin.managed_scripts.show', compact('script'));
    }

    /**
     * Show the form for editing the specified resource.
     * @param ManagedScript $script
     * @return \Illuminate\Contracts\View\View
     */
    public function edit(ManagedScript $script)
    {
        $statuses = ['development' => 'Development', 'active' => 'Active', 'beta' => 'Beta', 'deprecated' => 'Deprecated', 'archived' => 'Archived'];
        return view('appmanager::admin.managed_scripts.edit', compact('script', 'statuses'));
    }

    /**
     * Update the specified resource in storage.
     * @param Request $request
     * @param ManagedScript $script
     * @return \Illuminate\Http\RedirectResponse
     */
    public function update(Request $request, ManagedScript $script)
    {
        $request->validate([
            'name' => 'required|string|max:255|unique:am_managed_scripts,name,' . $script->id,
            'slug' => 'nullable|string|max:255|unique:am_managed_scripts,slug,' . $script->id,
            'current_version' => 'required|string|max:50',
            'description' => 'nullable|string',
            'changelog' => 'nullable|string',
            'is_boilerplate_core' => 'boolean',
            'status' => 'required|in:development,active,beta,deprecated,archived',
            'envato_item_id' => 'nullable|string|max:50', // Add validation
        ]);

        $data = $request->all();
        $data['slug'] = $request->input('slug') ?: Str::slug($request->input('name'));
        $data['is_boilerplate_core'] = $request->has('is_boilerplate_core');

        $script->update($data);

        return redirect()->route('admin.appmanager.scripts.index')
                         ->with('success', 'Managed script updated successfully.');
    }

    /**
     * Remove the specified resource from storage.
     * @param ManagedScript $script
     * @return \Illuminate\Http\RedirectResponse
     */
    public function destroy(ManagedScript $script)
    {
        // Consider what happens to associated licenses and files.
        // Soft delete or prevent deletion if dependencies exist?
        $script->delete();
        return redirect()->route('admin.appmanager.scripts.index')
                         ->with('success', 'Managed script deleted successfully.');
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\ActivationController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http; // For Envato API call
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Modules\AppManager\Entities\License;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\ActivationLog;
use Modules\AppManager\Entities\DownloadToken; // Import DownloadToken
use Illuminate\Http\JsonResponse;

class ActivationController extends Controller
{
    public function activate(Request $request): JsonResponse
    {
        Log::debug('AppManager Activation Request Data:', $request->all());
        Log::debug('AppManager Activation Request Content:', ['content' => $request->getContent()]); // Log raw content
        Log::debug('AppManager Activation Request Headers:', $request->headers->all());

        $validator = Validator::make($request->all(), [
            'license_key' => 'required_without:purchase_code|string|max:255',
            'purchase_code' => 'required_without:license_key|string|max:255',
            'product_slug' => 'required|string|exists:am_managed_scripts,slug',
            'domain' => 'required|string|max:255', // Consider validating as URL/domain
            'server_signature' => 'nullable|string|max:255', // Optional hash of server info
        ]);

        if ($validator->fails()) {
            return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400);
        }

        $product = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail();
        $license = null;
        $envatoToken = config('appmanager.envato_personal_token');
        $purchaseCode = trim($request->input('purchase_code')); // Trim whitespace

        if ($request->filled('license_key')) {
            $license = License::where('license_key', $request->input('license_key'))
                              ->where('managed_script_id', $product->id)
                              ->first();
        } elseif ($request->filled('purchase_code') && $purchaseCode) {
            // Validate purchase code format before sending to Envato
            if (!preg_match("/^([a-f0-9]{8})-(([a-f0-9]{4})-){3}([a-f0-9]{12})$/i", $purchaseCode)) {
                $errorMsg = "Invalid purchase code format.";
                Log::info('AppManager: Invalid purchase code format received.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'ip' => $request->ip()]);
                $this->logActivationAttempt($request, $product, null, 'failed_invalid_purchase_code_format', $errorMsg);
                return response()->json(['status' => 'error', 'message' => $errorMsg], 400); // 400 for bad request
            }

            if ($product->envato_item_id && $envatoToken) {
                try {
                    $envatoResponse = Http::withToken($envatoToken)
                        ->withUserAgent(config('app.name', 'Laravel') . ' - AppManager Purchase Verification')
                        ->timeout(15) // Set a reasonable timeout
                        ->get("https://api.envato.com/v3/market/author/sale?code={$purchaseCode}");

                    if ($envatoResponse->successful()) {
                        $saleData = $envatoResponse->json();
                        if (isset($saleData['item']['id']) && $saleData['item']['id'] == $product->envato_item_id) {
                            // Purchase code is valid for this item
                            $baseLicenseData = [
                                'type' => 'codecanyon',
                                'customer_email' => $saleData['buyer'] ?? null,
                                'customer_name' => $saleData['buyer_username'] ?? ($saleData['buyer'] ?? null),
                                'status' => 'active', // Default to active on successful verification
                                'supported_until' => isset($saleData['supported_until']) ? \Carbon\Carbon::parse($saleData['supported_until']) : null,
                            ];

                            // Find existing license by purchase code for this product, or create a new one
                            $license = License::updateOrCreate(
                                ['purchase_code' => $purchaseCode, 'managed_script_id' => $product->id],
                                $baseLicenseData // These are values for creation or fields to be updated if found
                            );

                            // Ensure metadata is correctly updated/set.
                            // updateOrCreate handles the other fields from $baseLicenseData.
                            $newMetadata = $license->metadata ?? [];
                            $newMetadata['envato_license_type'] = $saleData['license'] ?? null;
                            $license->metadata = $newMetadata;

                            // Save if metadata changed or if updateOrCreate made changes not yet persisted
                            // (though updateOrCreate typically persists immediately). A single save here is robust.
                            $license->save();
                        } else {
                            // Valid response from Envato, but item ID doesn't match
                            $errorMsg = 'Purchase code is valid but for a different product.';
                            Log::warning('AppManager: Envato purchase code product mismatch.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'expected_item_id' => $product->envato_item_id, 'received_item_id' => $saleData['item']['id'] ?? 'N/A', 'ip' => $request->ip()]);
                            $this->logActivationAttempt($request, $product, null, 'failed_envato_product_mismatch', $errorMsg);
                            return response()->json(['status' => 'error', 'message' => $errorMsg], 403);
                        }
                    } else { // Envato request was not successful
                        $statusCode = $envatoResponse->status();
                        $envatoErrorBody = $envatoResponse->json() ?? ['description' => $envatoResponse->body()];
                        $errorDescription = $envatoErrorBody['description'] ?? 'Unknown Envato API error.';

                        if ($statusCode === 404) {
                            $errorMsg = 'Invalid purchase code or it does not belong to your items.';
                        } elseif ($statusCode === 403) {
                            $errorMsg = 'Envato API permission issue or token invalid. Please check your Envato token.';
                        } elseif ($statusCode === 401) {
                            $errorMsg = 'Envato API authentication failed. Please check your authorization header.';
                        } else {
                            $errorMsg = "Envato API error ({$statusCode}): {$errorDescription}";
                        }

                        Log::warning('AppManager: Envato API request failed.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'status_code' => $statusCode, 'response_body' => $envatoErrorBody, 'ip' => $request->ip()]);
                        $this->logActivationAttempt($request, $product, null, "failed_envato_api_s{$statusCode}", $errorMsg);
                        return response()->json(['status' => 'error', 'message' => $errorMsg], $statusCode >= 500 ? 503 : 403);
                    }
                } catch (\Illuminate\Http\Client\ConnectionException $e) {
                    Log::error('AppManager: Envato API connection error.', ['error' => $e->getMessage(), 'ip' => $request->ip()]);
                    $this->logActivationAttempt($request, $product, null, 'failed_envato_api_connection', 'Could not connect to verification server.');
                    return response()->json(['status' => 'error', 'message' => 'Could not connect to verification server. Please try again later.'], 503);
                }
            } else {
                 // Fallback to checking existing licenses if Envato details are not configured for the product
                 $license = License::where('purchase_code', $purchaseCode)
                                   ->where('managed_script_id', $product->id)
                                   ->first();
            }
        }

        if (!$license) {
            $this->logActivationAttempt($request, $product, null, 'failed_license_not_found', 'License key or purchase code not found or invalid for this product.');
            return response()->json(['status' => 'error', 'message' => 'License key or purchase code not found or invalid for this product.'], 403);
        }

        // Check license status
        if (!in_array($license->status, ['pending', 'active'])) {
            $this->logActivationAttempt($request, $product, $license, 'failed_license_status_inactive', 'License is not active (Status: '.$license->status.').');
            return response()->json(['status' => 'error', 'message' => 'License is not active. Current status: ' . $license->status], 403);
        }

        // Check expiry
        if ($license->expires_at && $license->expires_at->isPast()) {
            $this->logActivationAttempt($request, $product, $license, 'failed_license_expired', 'License has expired.');
            return response()->json(['status' => 'error', 'message' => 'License has expired.'], 403);
        }

        // Check activation limit (0 means unlimited)
        $domainToActivate = strtolower($request->input('domain'));
        $existingActivationsForDomain = $license->activationLogs()->where('activated_domain', $domainToActivate)->where('status', 'success')->exists(); // Ensure this uses the correct relationship name

        if (!$existingActivationsForDomain && $license->activation_limit > 0 && $license->current_activations >= $license->activation_limit) {
            $this->logActivationAttempt($request, $product, $license, 'failed_activation_limit_reached', 'Activation limit reached for this license.');
            return response()->json(['status' => 'error', 'message' => 'Activation limit reached for this license.'], 403);
        }

        // Log successful activation
        $activationLog = $this->logActivationAttempt($request, $product, $license, 'success', 'Activation successful.');

        if (!$existingActivationsForDomain) {
            $license->increment('current_activations');
        }
        if ($license->status === 'pending') {
            $license->status = 'active';
        }
        $license->save();

        // Generate a DownloadToken
        $downloadToken = DownloadToken::create([
            'license_id' => $license->id,
            // 'activation_log_id' => $activationLog->id, // Optional: If you add this FK to DownloadToken
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'expires_at' => now()->addMinutes(config('appmanager.download_token_ttl_minutes', 30)),
            'max_uses' => config('appmanager.download_token_max_uses', 5), // Allow a few uses for a set of files
        ]);
        $activationToken = $downloadToken->token;


        return response()->json([
            'status' => 'success',
            'message' => 'Application activated successfully.',
            'activation_token' => $activationToken, // For subsequent file downloads
            'product_info' => [
                'name' => $product->name,
                'version' => $product->current_version,
            ],
            'license_info' => [
                'status' => $license->status, 'expires_at' => $license->expires_at ? $license->expires_at->toIso8601String() : null,
                'supported_until' => $license->supported_until ? $license->supported_until->toIso8601String() : null,
                'type' => $license->type,
                'envato_license_type' => $license->metadata['envato_license_type'] ?? null
            ]
        ]);
    }

    public function validateLicense(Request $request): JsonResponse
    {
        // Similar validation as activate, but doesn't increment activation count
        // Just checks if the license is still valid for the given domain/product
        $validator = Validator::make($request->all(), [
            'license_key' => 'required|string|max:255',
            'product_slug' => 'required|string|exists:am_managed_scripts,slug',
            'domain' => 'required|string|max:255',
        ]);

        if ($validator->fails()) {
            return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400);
        }

        $product = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail();
        $license = License::where('license_key', $request->input('license_key'))
                          ->where('managed_script_id', $product->id)
                          ->first();

        if (!$license || $license->status !== 'active' || ($license->expires_at && $license->expires_at->isPast())) {
            return response()->json(['status' => 'invalid', 'message' => 'License is invalid or expired.'], 403);
        }

        // Optionally, check if this domain is among the activated ones for this license
        $isActiveForDomain = $license->activationLogs()
                                    ->where('activated_domain', strtolower($request->input('domain')))
                                    ->where('status', 'success')
                                    ->exists();

        if (!$isActiveForDomain) {
             return response()->json(['status' => 'invalid', 'message' => 'License not activated for this domain.'], 403);
        }

        return response()->json(['status' => 'valid', 'message' => 'License is active.']);
    }

    private function logActivationAttempt(Request $request, ManagedScript $product, ?License $license, string $status, string $message): ActivationLog
    {
        return ActivationLog::create([
            'license_id' => $license?->id,
            'managed_script_id' => $product->id, // Always log the product context
            'license_key_attempt' => $request->input('license_key'),
            'purchase_code_attempt' => trim($request->input('purchase_code')), // Log trimmed code
            'activated_domain' => strtolower($request->input('domain')),
            'ip_address' => $request->ip(),
            'server_signature' => $request->input('server_signature'),
            'status' => $status,
            'message' => $message,
            'activated_at' => now(), // This will be handled by am_activation_logs table definition if it has `useCurrent`
        ]);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\ActivationValidationController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Api;

use App\Http\Controllers\Controller; // Assuming your base controller is in App\Http\Controllers
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Modules\AppManager\Models\License; // Assuming you have a License model
use Modules\AppManager\Models\ManagedScript; // Assuming you have a ManagedScript model for products

class ActivationValidationController extends Controller
{
    public function validateActivation(Request $request)
    {
        $request->validate([
            'activation_token' => 'required|string',
            'domain' => 'required|string|url',
            'product_slug' => 'required|string',
        ]);

        $activationToken = $request->input('activation_token');
        $domain = strtolower(trim($request->input('domain')));
        $productSlug = $request->input('product_slug');

        try {
            $license = License::where('activation_token', $activationToken)->first();

            if (!$license) {
                return response()->json([
                    'status' => 'error',
                    'is_valid' => false,
                    'message' => 'Activation token not found.',
                ], 404);
            }

            // Assuming License model has a relationship to ManagedScript (product)
            // and ManagedScript has a 'slug' field.
            if (!$license->managedScript || $license->managedScript->slug !== $productSlug) {
                return response()->json([
                    'status' => 'error',
                    'is_valid' => false,
                    'message' => 'Product slug mismatch for this token.',
                ], 400);
            }

            // Ensure domain is consistently stored/checked in lowercase
            if (strtolower(trim($license->activated_domain)) !== $domain) {
                return response()->json([
                    'status' => 'error',
                    'is_valid' => false,
                    'message' => 'Domain mismatch for this token.',
                ], 400);
            }

            // Check license status (e.g., 'active', 'expired', 'revoked')
            // This logic depends on how you manage license statuses in your AppManager
            if ($license->status !== 'active') { // Adjust 'active' to your actual active status value
                return response()->json([
                    'status' => 'error',
                    'is_valid' => false,
                    'message' => "License is not active. Current status: {$license->status}.",
                ], 400);
            }

            // If all checks pass
            Log::info('AppManager: Activation token validated successfully.', ['token' => $activationToken, 'domain' => $domain, 'product_slug' => $productSlug]);
            return response()->json([
                'status' => 'success',
                'is_valid' => true,
                'message' => 'Token is valid.',
            ]);

        } catch (\Exception $e) {
            Log::error('AppManager: Error during activation validation API call.', ['error' => $e->getMessage(), 'token' => $activationToken, 'domain' => $domain]);
            return response()->json(['status' => 'error', 'is_valid' => false, 'message' => 'An internal error occurred during validation.'], 500);
        }
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\FileDownloadController.php ---
<?php

namespace Modules\AppManager\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\DownloadableFile;
use Modules\AppManager\Entities\DownloadToken; // Import DownloadToken

class FileDownloadController extends Controller
{
    protected string $storageDisk;
    protected string $baseStoragePath = 'app_manager_files'; // Base path within the disk

    public function __construct()
    {
        $this->storageDisk = config('appmanager.storage_disk', 'local');
    }

    public function download(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'product_slug' => 'required|string|exists:am_managed_scripts,slug',
            'file_identifier' => 'required|string', // This could be the target_path or a unique ID/file_name
            'version' => 'required|string',
            'activation_token' => 'required|string', // The token received from successful activation
            'license_key' => 'sometimes|required|string', // Optional: For an extra layer of validation if token is tied to license
            'domain' => 'sometimes|required|string',      // Optional: If token is tied to a domain
        ]);

        if ($validator->fails()) {
            return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400);
        }

        // --- Robust Token Validation ---
        $downloadToken = DownloadToken::where('token', $request->input('activation_token'))->first();

        if (!$downloadToken) {
            Log::warning('AppManager: Invalid download token used.', ['token' => $request->input('activation_token'), 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Invalid or expired download token.'], 403);
        }

        if ($downloadToken->isExpired()) {
            Log::info('AppManager: Expired download token used.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Download token has expired.'], 403);
        }

        if ($downloadToken->isMaxUsesReached()) {
            Log::info('AppManager: Download token max uses reached.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]);
            return response()->json(['status' => 'error', 'message' => 'Download token usage limit reached.'], 403);
        }

        // Optional: Further checks if token is tied to specific license_key or domain
        // This requires the DownloadToken to have a direct relationship or stored metadata
        // For example, if DownloadToken belongs to a License:
        if ($request->filled('license_key') && $downloadToken->license->license_key !== $request->input('license_key')) {
             Log::warning('AppManager: Download token license mismatch.', [
                'token_id' => $downloadToken->id,
                'expected_key' => $downloadToken->license->license_key,
                'provided_key' => $request->input('license_key'),
                'ip' => $request->ip()
            ]);
            return response()->json(['status' => 'error', 'message' => 'Token not valid for this license.'], 403);
        }
        // You might also check if the token was generated for the correct product_slug via $downloadToken->license->managed_script_id

        // --- End Robust Token Validation ---


        $script = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail();
        $downloadableFile = DownloadableFile::where('managed_script_id', $script->id)
                                            ->where(function($query) use ($request) {
                                                // Allow lookup by target_path primarily, or file_name as a fallback
                                                $query->where('target_path', $request->input('file_identifier'))
                                                      ->orWhere('file_name', $request->input('file_identifier'));
                                            })
                                            ->where('version', $request->input('version'))
                                            ->first();

        if (!$downloadableFile) {
            Log::info('AppManager: Downloadable file not found or version mismatch.', $request->all());
            return response()->json(['status' => 'error', 'message' => 'File not found or version mismatch.'], 404);
        }

        $filePathOnDisk = $downloadableFile->file_path; // This path is relative to the disk's root

        if (!Storage::disk($this->storageDisk)->exists($filePathOnDisk)) {
            Log::error("AppManager: File source not found on server for download.", ['path' => $filePathOnDisk, 'request' => $request->all()]);
            return response()->json(['status' => 'error', 'message' => 'File source not found on server.'], 500);
        }

        // Optional: Verify hash before sending
        $currentHash = hash_file('sha256', Storage::disk($this->storageDisk)->path($filePathOnDisk));
        if ($downloadableFile->file_hash && $currentHash !== $downloadableFile->file_hash) {
            Log::critical("AppManager: CRITICAL - File hash mismatch for download.", [
                'file_record_id' => $downloadableFile->id,
                'path' => $filePathOnDisk,
                'stored_hash' => $downloadableFile->file_hash,
                'current_hash' => $currentHash
            ]);
            return response()->json(['status' => 'error', 'message' => 'File integrity check failed on server.'], 500);
        }
        
        $downloadToken->incrementUsage();
        // Optionally, delete the token if it has reached max uses and you want it to be strictly single-session for a set of downloads
        // if ($downloadToken->isMaxUsesReached()) { $downloadToken->delete(); }

        return Storage::disk($this->storageDisk)->download($filePathOnDisk, $downloadableFile->file_name, [
            'Content-Type' => Storage::disk($this->storageDisk)->mimeType($filePathOnDisk) ?? 'application/octet-stream',
            'X-File-Hash' => $downloadableFile->file_hash // Send hash for client-side verification
        ]);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\AppManagerController.php ---
<?php

namespace Modules\AppManager\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class AppManagerController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('appmanager::index');
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('appmanager::create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request) {}

    /**
     * Show the specified resource.
     */
    public function show($id)
    {
        return view('appmanager::show');
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        return view('appmanager::edit');
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id) {}

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id) {}
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Providers\AppManagerServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory; // Keep if you plan to use factories

class AppManagerServiceProvider extends ServiceProvider
{
    /**
     * @var string $moduleName
     */
    protected string $moduleName = 'AppManager';

    /**
     * @var string $moduleNameLower
     */
    protected string $moduleNameLower = 'appmanager';

    /**
     * Boot the application events.
     *
     * @return void
     */
    public function boot(): void
    {
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations')); // Keep migrations
        // The module's own RouteServiceProvider (registered in the register() method)
        // will handle loading its web.php routes.
        // The main application's RouteServiceProvider handles loading admin.php and api.php for all modules.
        $this->publishAssets();
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register(): void
    {
        $this->app->register(RouteServiceProvider::class);
    }

    /**
     * Register config.
     *
     * @return void
     */
    protected function registerConfig(): void
    {
        $this->publishes([
            module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'),
        ], 'config');
        $this->mergeConfigFrom(
            module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower
        );
    }

    /**
     * Register views.
     *
     * @return void
     */
    public function registerViews(): void
    {
        $viewPath = resource_path('views/modules/' . $this->moduleNameLower);
        $sourcePath = module_path($this->moduleName, 'resources/views');

        $this->publishes([
            $sourcePath => $viewPath
        ], ['views', $this->moduleNameLower . '-module-views']);

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
    }

    /**
     * Register translations.
     *
     * @return void
     */
    public function registerTranslations(): void
    {
        // Similar to LandingPageServiceProvider, adapt if needed
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides(): array
    {
        return [];
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (\Config::get('view.paths') as $path) {
            if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
                $paths[] = $path . '/modules/' . $this->moduleNameLower;
            }
        }
        return $paths;
    }

    protected function publishAssets(): void
    {
        $this->publishes([
            module_path($this->moduleName, 'public') => public_path('modules/'. $this->moduleNameLower),
        ], ['public', $this->moduleNameLower . '-module-assets']);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Providers\EventServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event handler mappings for the application.
     *
     * @var array<string, array<int, string>>
     */
    protected $listen = [];

    /**
     * Indicates if events should be discovered.
     *
     * @var bool
     */
    protected static $shouldDiscoverEvents = true;

    /**
     * Configure the proper event listeners for email verification.
     */
    protected function configureEmailVerification(): void {}
}


--- File: D:\projects\digitalvocano\Modules\AppManager\Providers\RouteServiceProvider.php ---
<?php

namespace Modules\AppManager\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    protected string $name = 'AppManager';

    /**
     * Called before routes are registered.
     *
     * Register any model bindings or pattern based filters.
     */
    public function boot(): void
    {
        parent::boot();
    }

    /**
     * Define the routes for the application.
     */
    public function map(): void
    {
        // API routes are mapped by the main application's RouteServiceProvider,
        // which adds the necessary 'api/module-name' prefix.
        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application.
     *
     * These routes all receive session state, CSRF protection, etc.
     */
    protected function mapWebRoutes(): void
    {
        Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
    }

    /**
     * Define the "api" routes for the application.
     *
     * These routes are typically stateless.
     */
    protected function mapApiRoutes(): void
    {
        Route::middleware('api')
            // ->namespace($this->moduleNamespace . '\Api') // If your API controllers are in an 'Api' subfolder
            ->group(module_path($this->name, '/routes/api.php'));
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\activation_logs\index.blade.php ---
@extends('layouts.admin')

@section('title', 'Activation Logs')
@section('header_title', 'Script Activation Logs')

@section('content')
<div class="mb-4 flex justify-between items-center">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">Activation Logs</h2>
</div>

@include('admin.partials.alerts')

<!-- Search and Filter Form -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
    <form method="GET" action="{{ route('admin.appmanager.activationlogs.index') }}">
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
            <div>
                <label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search (Domain, IP, Key, Email)</label>
                <input type="text" name="search" id="search" placeholder="Enter search term..." value="{{ request('search') }}"
                       class="mt-1 block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50">
            </div>
            <div>
                <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
                <select name="status" id="status" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
                    <option value="all" {{ request('status') == 'all' ? 'selected' : '' }}>All Statuses</option>
                    @foreach($statuses as $key => $value)
                        <option value="{{ $key }}" {{ request('status') == $key ? 'selected' : '' }}>{{ $value }}</option>
                    @endforeach
                </select>
            </div>
            <div class="flex items-end">
                <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
                    Filter
                </button>
                 <a href="{{ route('admin.appmanager.activationlogs.index') }}" class="ml-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">
                    Clear
                </a>
            </div>
        </div>
    </form>
</div>


<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>
                <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
                    <th class="px-4 py-3">Script</th>
                    <th class="px-4 py-3">License Key</th>
                    <th class="px-4 py-3">Domain</th>
                    <th class="px-4 py-3">IP Address</th>
                    <th class="px-4 py-3">Status</th>
                    <th class="px-4 py-3">Message</th>
                    <th class="px-4 py-3">Date</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
                @forelse ($logs as $log)
                <tr class="text-gray-700 dark:text-gray-400">
                    <td class="px-4 py-3 text-sm">{{ $log->license->managedScript->name ?? 'N/A' }}</td>
                    <td class="px-4 py-3 text-sm">{{ $log->license->license_key ?? 'N/A' }}</td>
                    <td class="px-4 py-3 text-sm">{{ $log->activated_domain ?: '-' }}</td>
                    <td class="px-4 py-3 text-sm">{{ $log->ip_address ?: '-' }}</td>
                    <td class="px-4 py-3 text-xs">
                        <span class="px-2 py-1 font-semibold leading-tight
                            @if($log->status == 'success') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                            @else text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700 @endif
                            rounded-full">
                            {{ ucfirst(str_replace('_', ' ', $log->status)) }}
                        </span>
                    </td>
                    <td class="px-4 py-3 text-sm truncate max-w-xs" title="{{ $log->message }}">{{ Str::limit($log->message, 50) ?: '-' }}</td>
                    <td class="px-4 py-3 text-sm">{{ $log->activated_at->format('M d, Y H:i') }}</td>
                </tr>
                @empty
                <tr>
                    <td colspan="7" class="px-4 py-3 text-center text-sm text-gray-500 dark:text-gray-400">No activation logs found.</td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>
    <div class="px-4 py-3">
        {{ $logs->links() }}
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\create.blade.php ---
@extends('layouts.admin')

@section('title', 'Create New Managed Script')
@section('header_title', 'Create New Managed Script')

@section('content')
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        @include('admin.partials.alerts')
        <form action="{{ route('admin.appmanager.scripts.store') }}" method="POST">
            @csrf
            @include('appmanager::admin.managed_scripts._form', ['script' => null, 'statuses' => $statuses])
        </form>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\edit.blade.php ---
@extends('layouts.admin')

@section('title', 'Edit Managed Script: ' . $script->name)
@section('header_title', 'Edit Managed Script: ' . $script->name)

@section('content')
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        @include('admin.partials.alerts')
        <form action="{{ route('admin.appmanager.scripts.update', $script) }}" method="POST">
            @csrf
            @method('PUT')
            @include('appmanager::admin.managed_scripts._form', ['script' => $script, 'statuses' => $statuses])
        </form>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\index.blade.php ---
@extends('layouts.admin')

@section('title', 'Manage Scripts')
@section('header_title', 'Manage Scripts & Applications')

@section('content')
<div class="mb-4 flex justify-between items-center">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">Scripts / Modules</h2>
    <a href="{{ route('admin.appmanager.scripts.create') }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
        Add New Script
    </a>
</div>

@include('admin.partials.alerts')

<!-- Search Form -->
<div class="mb-4">
    <form method="GET" action="{{ route('admin.appmanager.scripts.index') }}">
        <div class="flex">
            <input type="text" name="search" placeholder="Search scripts..." value="{{ request('search') }}"
                   class="block w-full md:w-1/2 lg:w-1/3 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-l-md focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50">
            <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-r-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
                Search
            </button>
        </div>
    </form>
</div>

<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>
                <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
                    <th class="px-4 py-3">Name</th>
                    <th class="px-4 py-3">Slug</th>
                    <th class="px-4 py-3">Version</th>
                    <th class="px-4 py-3">Status</th>
                    <th class="px-4 py-3">Boilerplate Core</th>
                    <th class="px-4 py-3">Actions</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
                @forelse ($scripts as $script)
                <tr class="text-gray-700 dark:text-gray-400">
                    <td class="px-4 py-3 text-sm font-semibold">{{ $script->name }}</td>
                    <td class="px-4 py-3 text-sm">{{ $script->slug }}</td>
                    <td class="px-4 py-3 text-sm">{{ $script->current_version }}</td>
                    <td class="px-4 py-3 text-xs">
                        <span class="px-2 py-1 font-semibold leading-tight
                            @if($script->status == 'active') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                            @elseif($script->status == 'development') text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100
                            @elseif($script->status == 'beta') text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100
                            @else text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700 @endif
                            rounded-full">
                            {{ ucfirst($script->status) }}
                        </span>
                    </td>
                    <td class="px-4 py-3 text-sm">
                        {{ $script->is_boilerplate_core ? 'Yes' : 'No' }}
                    </td>
                    <td class="px-4 py-3">
                        <div class="flex items-center space-x-2 text-sm">
                            <a href="{{ route('admin.appmanager.scripts.files.index', $script) }}" class="text-xs px-2 py-1 font-medium leading-5 text-white transition-colors duration-150 bg-blue-500 border border-transparent rounded-md active:bg-blue-600 hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue" title="Manage Files">
                                Files
                            </a>
                            <a href="{{ route('admin.appmanager.scripts.edit', $script) }}" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Edit">
                                <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path></svg>
                            </a>
                            <form action="{{ route('admin.appmanager.scripts.destroy', $script) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this script? This might affect associated licenses and files.');">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Delete">
                                    <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
                                </button>
                            </form>
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="6" class="px-4 py-3 text-center text-sm text-gray-500 dark:text-gray-400">No managed scripts found.</td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>
    <div class="px-4 py-3">
        {{ $scripts->links() }}
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\show.blade.php ---
@extends('layouts.admin')

@section('title', 'Script Details: ' . $script->name)
@section('header_title', 'Script Details: ' . $script->name)

@section('content')
<div class="mb-4">
    <a href="{{ route('admin.appmanager.scripts.index') }}" class="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200">&larr; Back to Scripts</a>
</div>

<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        <h3 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">{{ $script->name }} <span class="text-sm text-gray-500 dark:text-gray-400">(v{{ $script->current_version }})</span></h3>
        <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Slug: {{ $script->slug }}</p>
        <p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Status: <span class="font-medium">{{ ucfirst($script->status) }}</span></p>

        @if($script->description)
        <div class="mt-4">
            <h4 class="font-semibold text-gray-700 dark:text-gray-300">Description:</h4>
            <div class="prose dark:prose-invert max-w-none text-sm text-gray-600 dark:text-gray-400">
                {!! nl2br(e($script->description)) !!}
            </div>
        </div>
        @endif

        @if($script->changelog)
        <div class="mt-6">
            <h4 class="font-semibold text-gray-700 dark:text-gray-300">Changelog:</h4>
            <div class="prose dark:prose-invert max-w-none text-sm text-gray-600 dark:text-gray-400 p-4 border rounded-md dark:border-gray-700 mt-2 bg-gray-50 dark:bg-gray-700/50 max-h-96 overflow-y-auto">
                {!! nl2br(e($script->changelog)) !!} {{-- Consider Markdown parsing here --}}
            </div>
        </div>
        @endif

        <div class="mt-6 flex space-x-3">
            <a href="{{ route('admin.appmanager.scripts.edit', $script) }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
                Edit Script
            </a>
            <a href="{{ route('admin.appmanager.scripts.files.index', $script) }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-500 border border-transparent rounded-lg active:bg-blue-600 hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue">
                Manage Downloadable Files ({{ $script->downloadableFiles()->count() }})
            </a>
             <a href="{{ route('admin.appmanager.licenses.index', ['managed_script_id' => $script->id]) }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-green-500 border border-transparent rounded-lg active:bg-green-600 hover:bg-green-700 focus:outline-none focus:shadow-outline-green">
                View Licenses ({{ $script->licenses()->count() }})
            </a>
        </div>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\_form.blade.php ---
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    <div>
        <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Script Name <span class="text-red-500">*</span></label>
        <input type="text" name="name" id="name" value="{{ old('name', $script->name ?? '') }}" required
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('name') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Slug (URL friendly)</label>
        <input type="text" name="slug" id="slug" value="{{ old('slug', $script->slug ?? '') }}"
               placeholder="Leave blank to auto-generate from name"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('slug') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="current_version" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Current Version <span class="text-red-500">*</span></label>
        <input type="text" name="current_version" id="current_version" value="{{ old('current_version', $script->current_version ?? '1.0.0') }}" required
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('current_version') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status <span class="text-red-500">*</span></label>
        <select name="status" id="status" required
                class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
            @foreach($statuses as $key => $value)
                <option value="{{ $key }}" {{ old('status', $script->status ?? 'development') == $key ? 'selected' : '' }}>{{ $value }}</option>
            @endforeach
        </select>
        @error('status') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
        <textarea name="description" id="description" rows="4"
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('description', $script->description ?? '') }}</textarea>
        @error('description') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="changelog" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Changelog (Markdown supported)</label>
        <textarea name="changelog" id="changelog" rows="6"
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('changelog', $script->changelog ?? '') }}</textarea>
        @error('changelog') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="is_boilerplate_core" class="flex items-center cursor-pointer mt-1">
            <div class="relative">
                <input type="checkbox" id="is_boilerplate_core" name="is_boilerplate_core" class="sr-only" value="1" {{ old('is_boilerplate_core', isset($script) && $script->is_boilerplate_core ? '1' : '0') == '1' ? 'checked' : '' }}>
                <div class="block bg-gray-600 dark:bg-gray-700 w-14 h-8 rounded-full"></div>
                <div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
            </div>
            <div class="ml-3 text-gray-700 dark:text-gray-300 font-medium">
                Is Boilerplate Core License?
            </div>
        </label>
        <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Check if this script represents the main boilerplate application itself.</p>
        @error('is_boilerplate_core') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>
</div>

<div class="mt-8 flex justify-end">
    <a href="{{ route('admin.appmanager.scripts.index') }}" class="mr-3 inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150">
        Cancel
    </a>
    <button type="submit" class="inline-flex items-center px-4 py-2 bg-purple-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-purple-700 active:bg-purple-900 focus:outline-none focus:border-purple-900 focus:ring ring-purple-300 disabled:opacity-25 transition ease-in-out duration-150">
        {{ isset($script) ? 'Update Script' : 'Create Script' }}
    </button>
</div>

<style> /* Re-add toggle styles if not globally available in admin layout */
    input:checked ~ .dot { transform: translateX(100%); background-color: #48bb78; }
    input:checked ~ .block { background-color: #a0aec0; }
    .dark input:checked ~ .dot { background-color: #38a169; }
    .dark input:checked ~ .block { background-color: #4a5568; }
</style>


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\create.blade.php ---
@extends('layouts.admin')

@section('title', 'Create New License')
@section('header_title', 'Create New License')

@section('content')
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        @include('admin.partials.alerts')
        <form action="{{ route('admin.appmanager.licenses.store') }}" method="POST">
            @csrf
            @include('appmanager::admin.licenses._form', ['license' => null, 'scripts' => $scripts, 'types' => $types, 'statuses' => $statuses])
        </form>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\edit.blade.php ---
@extends('layouts.admin')

@section('title', 'Edit License: ' . $license->license_key)
@section('header_title', 'Edit License: ' . $license->license_key)

@section('content')
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        @include('admin.partials.alerts')
        <form action="{{ route('admin.appmanager.licenses.update', $license) }}" method="POST">
            @csrf
            @method('PUT')
            @include('appmanager::admin.licenses._form', ['license' => $license, 'scripts' => $scripts, 'types' => $types, 'statuses' => $statuses])
        </form>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\index.blade.php ---
@extends('layouts.admin')

@section('title', 'Manage Licenses')
@section('header_title', 'Manage Licenses')

@section('content')
<div class="mb-4 flex justify-between items-center">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">Licenses</h2>
    <a href="{{ route('admin.appmanager.licenses.create') }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
        Add New License
    </a>
</div>

@include('admin.partials.alerts')

<!-- Search and Filter Form -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
    <form method="GET" action="{{ route('admin.appmanager.licenses.index') }}">
        <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
            <div>
                <label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search (Key, Email, Purchase Code, Script Name)</label>
                <input type="text" name="search" id="search" placeholder="Enter search term..." value="{{ request('search') }}"
                       class="mt-1 block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50">
            </div>
            <div>
                <label for="managed_script_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Script</label>
                <select name="managed_script_id" id="managed_script_id" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
                    <option value="">All Scripts</option>
                    @foreach($scripts as $id => $name)
                        <option value="{{ $id }}" {{ request('managed_script_id') == $id ? 'selected' : '' }}>{{ $name }}</option>
                    @endforeach
                </select>
            </div>
            <div>
                <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
                <select name="status" id="status" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
                    <option value="all" {{ request('status') == 'all' ? 'selected' : '' }}>All Statuses</option>
                     @foreach($statuses as $key => $value)
                        <option value="{{ $key }}" {{ request('status') == $key ? 'selected' : '' }}>{{ $value }}</option>
                    @endforeach
                </select>
            </div>
            <div class="flex items-end">
                <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
                    Filter
                </button>
                <a href="{{ route('admin.appmanager.licenses.index') }}" class="ml-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">
                    Clear
                </a>
            </div>
        </div>
    </form>
</div>


<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>
                <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
                    <th class="px-4 py-3">License Key</th>
                    <th class="px-4 py-3">Script</th>
                    <th class="px-4 py-3">Customer</th>
                    <th class="px-4 py-3">Type</th>
                    <th class="px-4 py-3">Status</th>
                    <th class="px-4 py-3">Activations</th>
                    <th class="px-4 py-3">Expires At</th>
                    <th class="px-4 py-3">Actions</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
                @forelse ($licenses as $license)
                <tr class="text-gray-700 dark:text-gray-400">
                    <td class="px-4 py-3 text-sm font-semibold" title="{{ $license->license_key }}">{{ Str::limit($license->license_key, 25) }}</td>
                    <td class="px-4 py-3 text-sm">{{ $license->managedScript->name ?? 'N/A' }}</td>
                    <td class="px-4 py-3 text-sm">
                        {{ $license->customer_name ?: ($license->customer_email ?: 'N/A') }}
                        @if($license->customer_name && $license->customer_email)<br><span class="text-xs text-gray-500">{{ $license->customer_email }}</span>@endif
                    </td>
                    <td class="px-4 py-3 text-sm">{{ ucfirst($license->type) }}</td>
                    <td class="px-4 py-3 text-xs">
                        <span class="px-2 py-1 font-semibold leading-tight
                            @if($license->status == 'active') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                            @elseif(in_array($license->status, ['pending', 'suspended'])) text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100
                            @else text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700 @endif
                            rounded-full">
                            {{ ucfirst($license->status) }}
                        </span>
                    </td>
                    <td class="px-4 py-3 text-sm">{{ $license->current_activations }} / {{ $license->activation_limit == 0 ? 'Unlimited' : $license->activation_limit }}</td>
                    <td class="px-4 py-3 text-sm">{{ $license->expires_at ? $license->expires_at->format('M d, Y') : 'Never' }}</td>
                    <td class="px-4 py-3">
                        <div class="flex items-center space-x-2 text-sm">
                            <a href="{{ route('admin.appmanager.licenses.show', $license) }}" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="View">
                                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
                            </a>
                            <a href="{{ route('admin.appmanager.licenses.edit', $license) }}" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Edit">
                                <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path></svg>
                            </a>
                            <form action="{{ route('admin.appmanager.licenses.destroy', $license) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this license?');">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Delete">
                                    <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
                                </button>
                            </form>
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="8" class="px-4 py-3 text-center text-sm text-gray-500 dark:text-gray-400">No licenses found.</td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>
    <div class="px-4 py-3">
        {{ $licenses->links() }}
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\show.blade.php ---
@extends('layouts.admin')

@section('title', 'License Details: ' . $license->license_key)
@section('header_title', 'License Details: ' . Str::limit($license->license_key, 40))

@section('content')
<div class="mb-4">
    <a href="{{ route('admin.appmanager.licenses.index') }}" class="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200">&larr; Back to Licenses</a>
</div>

<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
    <div class="p-6 lg:p-8">
        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">License Key:</h4>
                <p class="text-gray-900 dark:text-gray-100 break-all">{{ $license->license_key }}</p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Script:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->managedScript->name ?? 'N/A' }}</p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Type:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ ucfirst($license->type) }}</p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Customer:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->customer_name ?: ($license->customer_email ?: 'N/A') }}</p>
                @if($license->customer_name && $license->customer_email)
                    <p class="text-xs text-gray-500 dark:text-gray-400">{{ $license->customer_email }}</p>
                @endif
            </div>
            @if($license->type === 'codecanyon')
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Purchase Code:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->purchase_code ?: 'N/A' }}</p>
            </div>
            @if(isset($license->metadata['envato_license_type']))
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Envato License:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->metadata['envato_license_type'] }}</p>
            </div>
            @endif
            @endif
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Status:</h4>
                <p class="px-2 py-1 inline-block text-xs font-semibold leading-tight
                    @if($license->status == 'active') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                    @elseif(in_array($license->status, ['pending', 'suspended'])) text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100
                    @else text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700 @endif
                    rounded-full">
                    {{ ucfirst($license->status) }}
                </p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Activations:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->current_activations }} / {{ $license->activation_limit == 0 ? 'Unlimited' : $license->activation_limit }}</p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Expires At:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->expires_at ? $license->expires_at->format('M d, Y H:i') : 'Never' }}</p>
            </div>
            <div>
                <h4 class="font-semibold text-gray-700 dark:text-gray-300">Support Until:</h4>
                <p class="text-gray-900 dark:text-gray-100">{{ $license->supported_until ? $license->supported_until->format('M d, Y') : 'N/A' }}</p>
            </div>
        </div>

        @if($license->metadata)
        <div class="mt-4 mb-6">
            <h4 class="font-semibold text-gray-700 dark:text-gray-300">Metadata:</h4>
            <pre class="text-xs bg-gray-100 dark:bg-gray-700 p-3 rounded-md overflow-x-auto"><code class="language-json">{{ json_encode($license->metadata, JSON_PRETTY_PRINT) }}</code></pre>
        </div>
        @endif

            <div class="mt-8 mb-6"> {{-- Adjusted margin and removed unnecessary col-span --}}
                <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Activated Domains:</h3>
                @php $activeDomains = $license->getActiveDomains(); @endphp
                @if($activeDomains->isNotEmpty())
                    <ul class="list-disc list-inside text-sm text-gray-600 dark:text-gray-400 mt-1 space-y-1">
                        @foreach($activeDomains as $domain)
                            <li>{{ $domain }}</li>
                        @endforeach
                    </ul>
                @else
                    <p class="text-sm text-gray-600 dark:text-gray-400 mt-1">No domains currently activated with this license.</p>
                @endif
            </div>

        <div class="mt-6">
            <a href="{{ route('admin.appmanager.licenses.edit', $license) }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
                Edit License
            </a>
        </div>
    </div>
</div>

<div class="mt-8">
    <h3 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">Activation Logs</h3>
    <div class="w-full overflow-hidden rounded-lg shadow-xs">
        <div class="w-full overflow-x-auto">
            <table class="w-full whitespace-no-wrap">
                <thead>
                    <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
                        <th class="px-4 py-3">Domain</th>
                        <th class="px-4 py-3">IP Address</th>
                        <th class="px-4 py-3">Status</th>
                        <th class="px-4 py-3">Message</th>
                        <th class="px-4 py-3">Date</th>
                    </tr>
                </thead>
                <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
                    @forelse ($license->activationLogs as $log)
                    <tr class="text-gray-700 dark:text-gray-400">
                        <td class="px-4 py-3 text-sm">{{ $log->activated_domain ?: '-' }}</td>
                        <td class="px-4 py-3 text-sm">{{ $log->ip_address ?: '-' }}</td>
                        <td class="px-4 py-3 text-xs">
                            <span class="px-2 py-1 font-semibold leading-tight {{ $log->status == 'success' ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700' }} rounded-full">
                                {{ ucfirst(str_replace('_', ' ', $log->status)) }}
                            </span>
                        </td>
                        <td class="px-4 py-3 text-sm truncate max-w-xs" title="{{ $log->message }}">{{ Str::limit($log->message, 50) ?: '-' }}</td>
                        <td class="px-4 py-3 text-sm">{{ $log->activated_at->format('M d, Y H:i') }}</td>
                    </tr>
                    @empty
                    <tr>
                        <td colspan="5" class="px-4 py-3 text-center text-sm text-gray-500 dark:text-gray-400">No activation logs found for this license.</td>
                    </tr>
                    @endforelse
                </tbody>
            </table>
        </div>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\_form.blade.php ---
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    <div>
        <label for="managed_script_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Managed Script <span class="text-red-500">*</span></label>
        <select name="managed_script_id" id="managed_script_id" required
                class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
            <option value="">Select Script</option>
            @foreach($scripts as $id => $name)
                <option value="{{ $id }}" {{ old('managed_script_id', $license->managed_script_id ?? request('managed_script_id')) == $id ? 'selected' : '' }}>{{ $name }}</option>
            @endforeach
        </select>
        @error('managed_script_id') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="license_key" class="block text-sm font-medium text-gray-700 dark:text-gray-300">License Key</label>
        <input type="text" name="license_key" id="license_key" value="{{ old('license_key', $license->license_key ?? '') }}"
               placeholder="Leave blank if auto-generating"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('license_key') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="auto_generate_key" class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
            <input type="checkbox" name="auto_generate_key" id="auto_generate_key" value="1"
                   class="rounded border-gray-300 text-purple-600 shadow-sm focus:border-purple-300 focus:ring focus:ring-purple-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600"
                   @if(old('auto_generate_key', !isset($license) || empty($license->license_key) )) checked @endif
                   onchange="toggleLicenseKeyInput(this.checked)">
            <span class="ml-2">Auto-generate license key</span>
        </label>
        <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">If checked, the license key field above will be ignored and a unique key generated.</p>
    </div>


    <div>
        <label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">License Type <span class="text-red-500">*</span></label>
        <select name="type" id="type" required
                class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
            @foreach($types as $key => $value)
                <option value="{{ $key }}" {{ old('type', $license->type ?? 'internal') == $key ? 'selected' : '' }}>{{ $value }}</option>
            @endforeach
        </select>
        @error('type') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="purchase_code" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Purchase Code (e.g., CodeCanyon)</label>
        <input type="text" name="purchase_code" id="purchase_code" value="{{ old('purchase_code', $license->purchase_code ?? '') }}"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('purchase_code') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="customer_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Customer Name</label>
        <input type="text" name="customer_name" id="customer_name" value="{{ old('customer_name', $license->customer_name ?? '') }}"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('customer_name') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="customer_email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Customer Email</label>
        <input type="email" name="customer_email" id="customer_email" value="{{ old('customer_email', $license->customer_email ?? '') }}"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('customer_email') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="activation_limit" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Activation Limit <span class="text-red-500">*</span></label>
        <input type="number" name="activation_limit" id="activation_limit" value="{{ old('activation_limit', $license->activation_limit ?? 1) }}" required min="0"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Set to 0 for unlimited activations.</p>
        @error('activation_limit') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status <span class="text-red-500">*</span></label>
        <select name="status" id="status" required
                class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
            @foreach($statuses as $key => $value)
                <option value="{{ $key }}" {{ old('status', $license->status ?? 'pending') == $key ? 'selected' : '' }}>{{ $value }}</option>
            @endforeach
        </select>
        @error('status') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Expires At</label>
        <input type="date" name="expires_at" id="expires_at" value="{{ old('expires_at', isset($license->expires_at) ? $license->expires_at->format('Y-m-d') : '') }}"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('expires_at') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-start-1"> {{-- Ensure this checkbox is on a new line or positioned logically --}}
        <label for="does_not_expire" class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer mt-2">
            <input type="checkbox" name="does_not_expire" id="does_not_expire" value="1"
                   class="rounded border-gray-300 text-purple-600 shadow-sm focus:border-purple-300 focus:ring focus:ring-purple-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600"
                   @if(old('does_not_expire', isset($license) && is_null($license->expires_at) && $license->exists)) checked @endif
                   onchange="toggleExpiresAtInput(this.checked)">
            <span class="ml-2">License does not expire</span>
        </label>
    </div>


    <div>
        <label for="supported_until" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Support Valid Until</label>
        <input type="date" name="supported_until" id="supported_until" value="{{ old('supported_until', isset($license->supported_until) ? $license->supported_until->format('Y-m-d') : '') }}"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('supported_until') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="metadata" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Metadata (JSON)</label>
        <textarea name="metadata" id="metadata" rows="3" placeholder='{"feature_x_enabled": true, "max_users": 10}'
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('metadata', isset($license->metadata) ? json_encode($license->metadata, JSON_PRETTY_PRINT) : '') }}</textarea>
        @error('metadata') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Internal Notes</label>
        <textarea name="notes" id="notes" rows="3"
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('notes', $license->notes ?? '') }}</textarea>
        @error('notes') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>
</div>

<div class="mt-8 flex justify-end">
    <a href="{{ route('admin.appmanager.licenses.index', ['managed_script_id' => request('managed_script_id', $license->managed_script_id ?? null)]) }}" class="mr-3 inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150">
        Cancel
    </a>
    <button type="submit" class="inline-flex items-center px-4 py-2 bg-purple-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-purple-700 active:bg-purple-900 focus:outline-none focus:border-purple-900 focus:ring ring-purple-300 disabled:opacity-25 transition ease-in-out duration-150">
        {{ isset($license) && $license->exists ? 'Update License' : 'Create License' }}
    </button>
</div>

@pushOnce('scripts')
<script>
    function toggleExpiresAtInput(doesNotExpire) {
        const expiresAtInput = document.getElementById('expires_at');
        if (expiresAtInput) {
            if (doesNotExpire) {
                expiresAtInput.disabled = true;
                expiresAtInput.value = ''; // Clear value if it doesn't expire
            } else {
                expiresAtInput.disabled = false;
            }
        }
    }

    function toggleLicenseKeyInput(autoGenerate) {
        const licenseKeyInput = document.getElementById('license_key');
        if (licenseKeyInput) {
            licenseKeyInput.disabled = autoGenerate;
            // The 'required' attribute is tricky with JS if the field is sometimes not submitted.
            // Server-side validation should handle if it's required when not auto-generating.
            // licenseKeyInput.required = !autoGenerate;
            if (autoGenerate) {
                licenseKeyInput.value = ''; // Clear if auto-generating
                licenseKeyInput.placeholder = 'Will be auto-generated';
            } else {
                 licenseKeyInput.placeholder = 'Enter license key manually';
            }
        }
    }

    document.addEventListener('DOMContentLoaded', function() {
        const doesNotExpireCheckbox = document.getElementById('does_not_expire');
        if (doesNotExpireCheckbox) {
            toggleExpiresAtInput(doesNotExpireCheckbox.checked);
        }

        const autoGenerateKeyCheckbox = document.getElementById('auto_generate_key');
        if (autoGenerateKeyCheckbox) {
            toggleLicenseKeyInput(autoGenerateKeyCheckbox.checked);
        }
    });
</script>
@endPushOnce


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\create.blade.php ---
@extends('layouts.admin') {{-- Or your AppManager specific admin layout if different --}}

@section('title', 'Create New Script/App')
@section('header_title', 'Create New Script/Application')

@section('content')
<div class="container px-6 mx-auto grid">
    <div class="my-6">
        <div class="p-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
            <h2 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-4">
                Script/Application Details
            </h2>

            <form method="POST" action="{{ route('admin.appmanager.scripts.store') }}">
                @csrf
                @include('appmanager::admin.managed_scripts._form', ['script' => null, 'statuses' => $statuses])
            </form>
        </div>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\edit.blade.php ---
@extends('layouts.admin') {{-- Or your AppManager specific admin layout if different --}}

@section('title', 'Edit Script: ' . $script->name)
@section('header_title', 'Edit Script/Application: ' . $script->name)

@section('content')
<div class="container px-6 mx-auto grid">
    <div class="my-6">
        <a href="{{ route('admin.appmanager.scripts.show', $script) }}" class="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200 mb-4 inline-block">
            &larr; Back to Script Details
        </a>
        <div class="p-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
            <h2 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-4">
                Edit {{ $script->name }}
            </h2>

            <form method="POST" action="{{ route('admin.appmanager.scripts.update', $script) }}">
                @csrf
                @method('PUT') {{-- Important for update operations --}}
                @include('appmanager::admin.managed_scripts._form', ['script' => $script, 'statuses' => $statuses])
            </form>
        </div>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\index.blade.php ---
@extends('layouts.admin')

@section('title', 'App Manager Dashboard')
@section('header_title', 'App Manager Dashboard')

@section('content')
<div class="mb-4 flex justify-between items-center">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">Welcome to App Manager</h2>
    <a href="{{ route('admin.appmanager.scripts.create') }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline-block mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
        </svg>
        Add New Script/App
    </a>
</div>

{{-- Removed include for non-existent partial --}}

<!-- Overview Stats -->
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
    <!-- Card 1: Total Scripts -->
    <div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
        <div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
            <x-appmanager::icons.managed-scripts />
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Managed Scripts</p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ $totalScriptsCount ?? ($scripts->total() ?? 'N/A') }}</p>
        </div>
    </div>

    <!-- Card 2: Total Licenses -->
    <div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
        <div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
            <x-appmanager::icons.licenses />
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Licenses Issued</p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ $totalLicensesCount ?? 'N/A' }}</p>
        </div>
    </div>
    <!-- Card 3: Recent Activations -->
    <div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
        <div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
            <x-appmanager::icons.recent-activations />
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Successful Activations (Last 7 Days)</p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ $recentSuccessfulActivationsCount ?? 'N/A' }}</p>
        </div>
    </div>
    <!-- Card 4: Quick Link to Activation Logs -->
    <a href="{{ route('admin.appmanager.activationlogs.index') }}" class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150">
        <div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
            <x-appmanager::icons.activation-logs />
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">View All</p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200">Activation Logs</p>
        </div>
    </a>
</div>

<!-- Search Form -->
<div class="mb-6 p-4 bg-white rounded-lg shadow-md dark:bg-gray-800">
    <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-3">Find a Script/Application</h3>
    <form method="GET" action="{{ route('admin.appmanager.scripts.index') }}">
        <div class="flex">
            <input type="text" name="search" placeholder="Search scripts..." value="{{ request('search') }}"
                   class="block w-full md:w-1/2 lg:w-1/3 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-l-md focus:border-purple-400 focus:ring focus:ring-purple-200 focus:ring-opacity-50">
            <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-r-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
                Search
            </button>
            @if(request('search'))
            <a href="{{ route('admin.appmanager.scripts.index') }}" class="ml-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">
                Clear
            </a>
            @endif
        </div>
    </form>
</div>

<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>
                <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
                    <th class="px-4 py-3">Name</th>
                    <th class="px-4 py-3">Slug</th>
                    <th class="px-4 py-3">Version</th>
                    <th class="px-4 py-3">Status</th>
                    <th class="px-4 py-3">Boilerplate Core</th>
                    <th class="px-4 py-3 text-center">Total Licenses</th>
                    <th class="px-4 py-3">Actions</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
                @forelse ($scripts as $script)
                <tr class="text-gray-700 dark:text-gray-400">
                    <td class="px-4 py-3 text-sm font-semibold">
                        <a href="{{ route('admin.appmanager.scripts.show', $script) }}" class="hover:text-purple-600 dark:hover:text-purple-400 font-bold">
                            {{ $script->name }}
                        </a>
                        <p class="text-xs text-gray-500 dark:text-gray-400">
                            @if($script->description)
                                {{ Str::limit($script->description, 70) }}
                            @else
                                No description.
                            @endif
                        </p>
                    </td>
                    <td class="px-4 py-3 text-sm">{{ $script->slug }}</td>
                    <td class="px-4 py-3 text-sm">{{ $script->current_version }}</td>
                    <td class="px-4 py-3 text-xs">
                        <span class="px-2 py-1 font-semibold leading-tight
                            @if($script->status == 'active') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                            @elseif($script->status == 'development') text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100
                            @elseif($script->status == 'beta') text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100
                            @else text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700 @endif
                            rounded-full">
                            {{ ucfirst($script->status) }}
                        </span>
                    </td>
                    <td class="px-4 py-3 text-sm">
                        {{ $script->is_boilerplate_core ? 'Yes' : 'No' }}
                    </td>
                    <td class="px-4 py-3 text-sm text-center">
                        <a href="{{ route('admin.appmanager.licenses.index', ['managed_script_id' => $script->id]) }}"
                           class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">
                           {{ $script->licenses_count ?? $script->licenses()->count() }}
                        </a>
                    </td>
                    <td class="px-4 py-3">
                        <div class="flex items-center space-x-2 text-sm">
                            <a href="{{ route('admin.appmanager.scripts.show', $script) }}" class="text-xs px-3 py-1 font-medium leading-5 text-white transition-colors duration-150 bg-gray-600 border border-transparent rounded-md active:bg-gray-700 hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray" title="View Details">
                                View
                            </a>
                            <a href="{{ route('admin.appmanager.scripts.files.index', $script) }}" class="text-xs px-3 py-1 font-medium leading-5 text-white transition-colors duration-150 bg-blue-500 border border-transparent rounded-md active:bg-blue-600 hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue" title="Manage Files">
                                Files
                            </a>
                            <a href="{{ route('admin.appmanager.scripts.edit', $script) }}" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Edit">
                                <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path></svg>
                            </a>
                            <form action="{{ route('admin.appmanager.scripts.destroy', $script) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this script? This might affect associated licenses and files.');">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray" aria-label="Delete">
                                    <svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
                                </button>
                            </form>
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="7" class="px-4 py-3 text-center text-sm text-gray-500 dark:text-gray-400">No managed scripts found. <a href="{{ route('admin.appmanager.scripts.create') }}" class="text-purple-600 hover:underline">Add your first script.</a></td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>
    <div class="px-4 py-3">
        {{ $scripts->links() }}
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\show.blade.php ---
@extends('layouts.admin') {{-- Or your AppManager specific admin layout --}}

@section('title', 'Script Details: ' . $script->name)
@section('header_title', 'Script Details: ' . $script->name)

@section('content')
<div class="container px-6 mx-auto grid">
    <div class="my-6">
        <div class="flex justify-between items-center mb-4">
            <a href="{{ route('admin.appmanager.scripts.index') }}" class="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200">
                &larr; Back to All Scripts
            </a>
            <div>
                <a href="{{ route('admin.appmanager.scripts.edit', $script) }}" class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
                    Edit Script
                </a>
                <a href="{{ route('admin.appmanager.scripts.files.index', $script) }}" class="ml-2 px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-500 border border-transparent rounded-lg active:bg-blue-600 hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue">
                    Manage Files
                </a>
            </div>
        </div>

        <div class="p-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
            <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-2">{{ $script->name }}</h2>
            <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Slug: {{ $script->slug }}</p>

            <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
                <div>
                    <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-1">Details</h3>
                    <dl class="divide-y divide-gray-200 dark:divide-gray-700">
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Current Version</dt>
                            <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:mt-0 sm:col-span-2">{{ $script->current_version ?: 'N/A' }}</dd>
                        </div>
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Status</dt>
                            <dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
                                <span class="px-2 py-1 font-semibold leading-tight
                                    @if($script->status == 'active') text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100
                                    @elseif($script->status == 'development') text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100
                                    @elseif($script->status == 'beta') text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100
                                    @elseif($script->status == 'deprecated') text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100
                                    @elseif($script->status == 'archived') text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700
                                    @else text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700 @endif
                                    rounded-full">
                                    {{ ucfirst($script->status) }}
                                </span>
                            </dd>
                        </div>
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Boilerplate Core</dt>
                            <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:mt-0 sm:col-span-2">{{ $script->is_boilerplate_core ? 'Yes' : 'No' }}</dd>
                        </div>
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Envato Item ID</dt>
                            <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:mt-0 sm:col-span-2">{{ $script->envato_item_id ?: 'N/A' }}</dd>
                        </div>
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Created At</dt>
                            <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:mt-0 sm:col-span-2">{{ $script->created_at->format('M d, Y H:i A') }}</dd>
                        </div>
                        <div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
                            <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Updated</dt>
                            <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:mt-0 sm:col-span-2">{{ $script->updated_at->format('M d, Y H:i A') }}</dd>
                        </div>
                    </dl>
                </div>
                <div>
                    <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-1">Description</h3>
                    <div class="prose prose-sm dark:prose-invert max-w-none p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
                        {!! $script->description ? nl2br(e($script->description)) : '<p class="italic text-gray-500 dark:text-gray-400">No description provided.</p>' !!}
                    </div>
                </div>
            </div>

            <div>
                <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">Changelog</h3>
                <div class="prose prose-sm dark:prose-invert max-w-none p-4 bg-gray-50 dark:bg-gray-700 rounded-md h-64 overflow-y-auto border dark:border-gray-600">
                    @if($script->changelog)
                        {!! \Illuminate\Support\Str::markdown(e($script->changelog)) !!}
                    @else
                        <p class="italic text-gray-500 dark:text-gray-400">No changelog provided.</p>
                    @endif
                </div>
            </div>

            <div class="mt-6">
                <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">Associated Licenses</h3>
                @if($script->licenses()->count() > 0)
                    <a href="{{ route('admin.appmanager.licenses.index', ['managed_script_id' => $script->id]) }}" class="text-purple-600 hover:underline dark:text-purple-400">
                        View {{ $script->licenses()->count() }} License(s)
                    </a>
                @else
                    <p class="text-sm text-gray-500 dark:text-gray-400">No licenses associated with this script yet.</p>
                    <a href="{{ route('admin.appmanager.licenses.create', ['managed_script_id' => $script->id]) }}" class="mt-2 inline-block px-3 py-1 text-xs font-medium leading-5 text-white transition-colors duration-150 bg-green-500 border border-transparent rounded-md active:bg-green-600 hover:bg-green-600 focus:outline-none focus:shadow-outline-green">
                        Create First License
                    </a>
                @endif
            </div>

        </div>
    </div>
</div>
@endsection


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\_form.blade.php ---
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    <div>
        <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Script Name <span class="text-red-500">*</span></label>
        <input type="text" name="name" id="name" value="{{ old('name', $script->name ?? '') }}" required
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('name') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Slug (URL friendly)</label>
        <input type="text" name="slug" id="slug" value="{{ old('slug', $script->slug ?? '') }}"
               placeholder="Leave blank to auto-generate from name"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('slug') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="current_version" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Current Version <span class="text-red-500">*</span></label>
        <input type="text" name="current_version" id="current_version" value="{{ old('current_version', $script->current_version ?? '1.0.0') }}" required
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('current_version') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status <span class="text-red-500">*</span></label>
        <select name="status" id="status" required
                class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
            @foreach($statuses as $key => $value)
                <option value="{{ $key }}" {{ old('status', $script->status ?? 'development') == $key ? 'selected' : '' }}>{{ $value }}</option>
            @endforeach
        </select>
        @error('status') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
        <textarea name="description" id="description" rows="4"
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('description', $script->description ?? '') }}</textarea>
        @error('description') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div class="md:col-span-2">
        <label for="changelog" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Changelog (Markdown supported)</label>
        <textarea name="changelog" id="changelog" rows="6"
                  class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">{{ old('changelog', $script->changelog ?? '') }}</textarea>
        @error('changelog') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="is_boilerplate_core" class="flex items-center cursor-pointer mt-1">
            <div class="relative">
                <input type="checkbox" id="is_boilerplate_core" name="is_boilerplate_core" class="sr-only" value="1" {{ old('is_boilerplate_core', isset($script) && $script->is_boilerplate_core ? '1' : '0') == '1' ? 'checked' : '' }}>
                <div class="block bg-gray-600 dark:bg-gray-700 w-14 h-8 rounded-full"></div>
                <div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
            </div>
            <div class="ml-3 text-gray-700 dark:text-gray-300 font-medium">
                Is Boilerplate Core License?
            </div>
        </label>
        <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Check if this script represents the main boilerplate application itself.</p>
        @error('is_boilerplate_core') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>

    <div>
        <label for="envato_item_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Envato Item ID</label>
        <input type="text" name="envato_item_id" id="envato_item_id" value="{{ old('envato_item_id', $script->envato_item_id ?? '') }}"
               placeholder="e.g., 12345678 (for CodeCanyon verification)"
               class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
        @error('envato_item_id') <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
    </div>
</div>

<div class="mt-8 flex justify-end">
    <a href="{{ route('admin.appmanager.scripts.index') }}" class="mr-3 inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150">
        Cancel
    </a>
    <button type="submit" class="inline-flex items-center px-4 py-2 bg-purple-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-purple-700 active:bg-purple-900 focus:outline-none focus:border-purple-900 focus:ring ring-purple-300 disabled:opacity-25 transition ease-in-out duration-150">
        {{ isset($script) ? 'Update Script' : 'Create Script' }}
    </button>
</div>

<style> /* Re-add toggle styles if not globally available in admin layout */
    input:checked ~ .dot { transform: translateX(100%); background-color: #48bb78; }
    input:checked ~ .block { background-color: #a0aec0; }
    .dark input:checked ~ .dot { background-color: #38a169; }
    .dark input:checked ~ .block { background-color: #4a5568; }
</style>


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\activation-logs.blade.php ---


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\licenses.blade.php ---


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\managed-scripts.blade.php ---


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\recent-activations.blade.php ---


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\layouts\master.blade.php ---
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">

        <title>AppManager Module - {{ config('app.name', 'Laravel') }}</title>

        <meta name="description" content="{{ $description ?? '' }}">
        <meta name="keywords" content="{{ $keywords ?? '' }}">
        <meta name="author" content="{{ $author ?? '' }}">

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        {{-- Vite CSS --}}
        {{-- {{ module_vite('build-appmanager', 'resources/assets/sass/app.scss') }} --}}
    </head>

    <body>
        {{ $slot }}

        {{-- Vite JS --}}
        {{-- {{ module_vite('build-appmanager', 'resources/assets/js/app.js') }} --}}
    </body>


--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\index.blade.php ---
<x-appmanager::layouts.master>
    <h1>Hello World</h1>

    <p>Module: {!! config('appmanager.name') !!}</p>
</x-appmanager::layouts.master>


--- File: D:\projects\digitalvocano\Modules\AppManager\routes\admin.php ---
<?php

use Illuminate\Support\Facades\Route;
use Modules\AppManager\Http\Controllers\Admin\ManagedScriptController;
use Modules\AppManager\Http\Controllers\Admin\LicenseController;
use Modules\AppManager\Http\Controllers\Admin\ActivationLogController;
use Modules\AppManager\Http\Controllers\Admin\DownloadableFileController;
use App\Http\Middleware\IsAdminMiddleware;

// Assuming a global 'admin/' prefix is applied by the main application

// The main app/Providers/RouteServiceProvider.php applies 'admin/appmanager/' prefix
// and 'admin.appmanager.' name prefix.
Route::middleware(['web', IsAdminMiddleware::class])
    ->group(function () {
        Route::resource('scripts', ManagedScriptController::class);
        Route::resource('scripts.files', DownloadableFileController::class)->except(['show']); // files nested under scripts

        Route::resource('licenses', LicenseController::class);
        Route::get('activation-logs', [ActivationLogController::class, 'index'])->name('activationlogs.index');
        // Add other AppManager admin routes here
    });

// Note: Ensure your controller namespaces are correct, e.g.,
// Modules\AppManager\Http\Controllers\Admin\ManagedScriptController


--- File: D:\projects\digitalvocano\Modules\AppManager\routes\api.php ---
<?php

use Illuminate\Support\Facades\Route;
use Modules\AppManager\Http\Controllers\Api\ActivationController;
use Modules\AppManager\Http\Controllers\Api\ActivationValidationController;
use Modules\AppManager\Http\Controllers\Api\FileDownloadController;

Route::group([], function () { 
    Route::post('activate', [ActivationController::class, 'activate'])->name('activate');
    Route::post('validate-license', [ActivationController::class, 'validateLicense'])->name('validate'); // For periodic checks
    Route::post('download-file', [FileDownloadController::class, 'download'])->name('download.file');
    Route::post('validate-activation', [ActivationValidationController::class, 'validateActivation'])->name('validate.activation');
});


--- File: D:\projects\digitalvocano\Modules\AppManager\routes\web.php ---
<?php

use Illuminate\Support\Facades\Route;
use Modules\AppManager\Http\Controllers\AppManagerController;

Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('appmanagers', AppManagerController::class)->names('appmanager');
});


--- File: D:\projects\digitalvocano\Modules\AppManager\tests\Feature\AppManagerApiTest.php ---
<?php

namespace Modules\AppManager\Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Modules\AppManager\Entities\ManagedScript;
use Modules\AppManager\Entities\License;
use Modules\AppManager\Entities\DownloadToken;
use Modules\AppManager\Entities\DownloadableFile;
    use Modules\AppManager\Entities\ActivationLog; // Import ActivationLog
use Tests\TestCase; // Your base TestCase from the main application

class AppManagerApiTest extends TestCase
{
    use RefreshDatabase;

    protected ManagedScript $script;
    protected License $license;

    protected function setUp(): void
    {
        parent::setUp();

        // Common setup for most tests
        $this->script = ManagedScript::factory()->create([
            'slug' => 'test-script',
            'current_version' => '1.0.0',
            'envato_item_id' => '12345678' // Example Envato ID
        ]);

        $this->license = License::factory()->create([
            'managed_script_id' => $this->script->id,
            'license_key' => 'VALID-LICENSE-KEY',
            'status' => 'active',
            'activation_limit' => 1,
            'current_activations' => 0,
        ]);
    }

    /**
     * @test
     */
    public function can_activate_with_valid_license_key(): void
    {
        $payload = [
            'product_slug' => $this->script->slug,
            'domain' => 'https://client.example.com',
            'license_key' => $this->license->license_key,
        ];

        $response = $this->postJson(route('api.appmanager.activate'), $payload);

        $response->assertStatus(200)
            ->assertJson([
                'status' => 'success',
                'message' => 'Application activated successfully.',
            ])
            ->assertJsonStructure([
                'status', 'message', 'activation_token',
                'product_info' => ['name', 'version'],
                'license_info' => ['status', 'expires_at', 'supported_until', 'type'],
            ]);

        $this->assertDatabaseHas('am_licenses', [
            'id' => $this->license->id,
            'current_activations' => 1,
        ]);
        $this->assertDatabaseHas('am_activation_logs', [
            'license_id' => $this->license->id,
            'activated_domain' => 'https://client.example.com',
            'status' => 'success',
        ]);
        $this->assertDatabaseCount('am_download_tokens', 1);
    }

    /**
     * @test
     */
    public function can_activate_with_valid_purchase_code_and_mocked_envato_success(): void
    {
        Http::fake([
            'api.envato.com/*' => Http::sequence()
                ->push([
                    'item' => ['id' => $this->script->envato_item_id],
                    'buyer' => 'testbuyer@example.com',
                    'supported_until' => now()->addYear()->toIso8601String(),
                    'license' => 'Regular License'
                ], 200)
        ]);

        $payload = [
            'product_slug' => $this->script->slug,
            'domain' => 'https://client-envato.example.com',
            'purchase_code' => 'VALID-PURCHASE-CODE-FORMAT-12345', // Format doesn't matter for mock
        ];

        $response = $this->postJson(route('api.appmanager.activate'), $payload);

        $response->assertStatus(200)
            ->assertJsonPath('status', 'success');

        $this->assertDatabaseHas('am_licenses', [
            'managed_script_id' => $this->script->id,
            'purchase_code' => 'VALID-PURCHASE-CODE-FORMAT-12345',
            'type' => 'codecanyon',
            'current_activations' => 1,
        ]);
        $this->assertDatabaseHas('am_activation_logs', [
            'purchase_code_attempt' => 'VALID-PURCHASE-CODE-FORMAT-12345',
            'activated_domain' => 'https://client-envato.example.com',
            'status' => 'success',
        ]);
    }


    /**
     * @test
     */
    public function activate_fails_if_license_not_found(): void
    {
        $payload = [
            'product_slug' => $this->script->slug,
            'domain' => 'https://client.example.com',
            'license_key' => 'NON-EXISTENT-KEY',
        ];

        $response = $this->postJson(route('api.appmanager.activate'), $payload);

        $response->assertStatus(403)
            ->assertJson(['status' => 'error', 'message' => 'License key or purchase code not found or invalid for this product.']);
    }

    /**
     * @test
     */
    public function activate_fails_if_activation_limit_reached(): void
    {
        $this->license->update(['current_activations' => 1, 'activation_limit' => 1]);

        $payload = [
            'product_slug' => $this->script->slug,
            'domain' => 'https://another-client.example.com',
            'license_key' => $this->license->license_key,
        ];

        $response = $this->postJson(route('api.appmanager.activate'), $payload);

        $response->assertStatus(403)
            ->assertJson(['status' => 'error', 'message' => 'Activation limit reached for this license.']);
    }

    /**
     * @test
     */
    public function activate_fails_with_invalid_payload(): void
    {
        $response = $this->postJson(route('api.appmanager.activate'), []);
        $response->assertStatus(400)
                 ->assertJsonValidationErrors(['product_slug', 'domain']);
    }

    /**
     * @test
     */
    public function can_validate_license_successfully(): void
    {
        // Simulate a successful activation first
        ActivationLog::factory()->create([
            'license_id' => $this->license->id,
            'managed_script_id' => $this->script->id,
            'activated_domain' => 'https://client.example.com',
            'status' => 'success',
        ]);

        $payload = [
            'license_key' => $this->license->license_key,
            'product_slug' => $this->script->slug,
            'domain' => 'https://client.example.com',
        ];

        $response = $this->postJson(route('api.appmanager.validate'), $payload);

        $response->assertStatus(200)
            ->assertJson(['status' => 'valid', 'message' => 'License is active.']);
    }

    /**
     * @test
     */
    public function validate_license_fails_if_not_active_for_domain(): void
    {
        $payload = [
            'license_key' => $this->license->license_key,
            'product_slug' => $this->script->slug,
            'domain' => 'https://unknown-domain.example.com', // Not activated for this domain
        ];

        $response = $this->postJson(route('api.appmanager.validate'), $payload);

        $response->assertStatus(403)
            ->assertJson(['status' => 'invalid', 'message' => 'License not activated for this domain.']);
    }

    /**
     * @test
     */
    public function can_download_file_with_valid_token(): void
    {
        Storage::fake(config('appmanager.storage_disk')); // Use fake storage

        $downloadableFile = DownloadableFile::factory()->create([
            'managed_script_id' => $this->script->id,
            'file_name' => 'test_file.zip',
            'file_path' => 'app_manager_files/' . $this->script->slug . '/1.0.0/test_file.zip',
            'version' => '1.0.0',
            'target_path' => 'app/test_file.zip',
        ]);
        // Create a dummy file in fake storage
        Storage::disk(config('appmanager.storage_disk'))->put($downloadableFile->file_path, 'dummy content');

        $downloadToken = DownloadToken::factory()->create([
            'license_id' => $this->license->id,
            'expires_at' => now()->addHour(),
            'max_uses' => 1,
        ]);

        $payload = [
            'activation_token' => $downloadToken->token,
            'product_slug' => $this->script->slug,
            'file_identifier' => $downloadableFile->target_path, // Assuming identifier is target_path
            'version' => $downloadableFile->version,
        ];

        $response = $this->postJson(route('api.appmanager.download.file'), $payload);

        $response->assertStatus(200)
                 ->assertHeader('Content-Disposition', 'attachment; filename="' . $downloadableFile->file_name . '"');
        $this->assertEquals('dummy content', $response->streamedContent());

        $this->assertDatabaseHas('am_download_tokens', [
            'id' => $downloadToken->id,
            'uses' => 1,
        ]);
    }

    /**
     * @test
     */
    public function download_file_fails_with_invalid_token(): void
    {
        $payload = [
            'activation_token' => 'INVALID-TOKEN',
            'product_slug' => $this->script->slug,
            'file_identifier' => 'app/some_file.zip',
            'version' => '1.0.0',
        ];

        $response = $this->postJson(route('api.appmanager.download.file'), $payload);
        $response->assertStatus(403); // Or 404 depending on controller logic
    }

    /**
     * @test
     */
    public function can_validate_activation_token_successfully(): void
    {
        $downloadToken = DownloadToken::factory()->create([
            'license_id' => $this->license->id,
            'expires_at' => now()->addHour(),
        ]);
        // Simulate an activation for the domain associated with this license
        ActivationLog::factory()->create([
            'license_id' => $this->license->id,
            'managed_script_id' => $this->script->id,
            'activated_domain' => 'https://client.example.com',
            'status' => 'success',
        ]);


        $payload = [
            'activation_token' => $downloadToken->token,
            'domain' => 'https://client.example.com',
            'product_slug' => $this->script->slug,
        ];

        $response = $this->postJson(route('api.appmanager.validate.activation'), $payload);

        $response->assertStatus(200)
            ->assertJson(['status' => 'success', 'is_valid' => true]);
    }

    /**
     * @test
     */
    public function validate_activation_token_fails_if_domain_mismatch(): void
    {
        $downloadToken = DownloadToken::factory()->create([
            'license_id' => $this->license->id,
            'expires_at' => now()->addHour(),
        ]);
        // Simulate an activation for a DIFFERENT domain
        ActivationLog::factory()->create([
            'license_id' => $this->license->id,
            'managed_script_id' => $this->script->id,
            'activated_domain' => 'https://original-activation.example.com',
            'status' => 'success',
        ]);

        $payload = [
            'activation_token' => $downloadToken->token,
            'domain' => 'https://different-client.example.com', // Mismatched domain
            'product_slug' => $this->script->slug,
        ];

        $response = $this->postJson(route('api.appmanager.validate.activation'), $payload);

        $response->assertStatus(403) // Or appropriate error code
            ->assertJson(['status' => 'error', 'is_valid' => false]);
    }
}


--- File: D:\projects\digitalvocano\Modules\AppManager\tests\Unit\AppManagerApiTest.php ---
<?php

namespace Modules\AppManager\Tests\Unit;

use Tests\TestCase;

class AppManagerApiTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_that_true_is_true(): void
    {
        $this->assertTrue(true);
    }
}


