#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

use Config ();
use Capture::Tiny qw(capture);
use Cwd qw(cwd);
use FindBin qw($Bin);
use lib "$Bin/../lib";
use File::Spec;
use Getopt::Long qw(GetOptionsFromArray);
use Getopt::Long ();
use IO::Handle ();
use IO::Select ();
use IPC::Open3 qw(open3);
use JSON::XS ();
use Pod::Usage qw(pod2usage);
use Symbol qw(gensym);

binmode STDOUT, ':encoding(UTF-8)';
binmode STDERR, ':encoding(UTF-8)';

use Developer::Dashboard::Platform qw(
  command_argv_for_path
  is_runnable_file
  native_shell_name
  normalize_shell_name
  resolve_runnable_file
  shell_quote_for
);
use Developer::Dashboard::InternalCLI ();

my $dashboard_lib = File::Spec->catdir( $Bin, '..', 'lib' );
if ( -d $dashboard_lib ) {
    my $path_sep = $Config::Config{path_sep} || ':';
    my @existing = grep { defined && $_ ne '' } split /\Q$path_sep\E/, ( $ENV{PERL5LIB} || '' );
    if ( !grep { $_ eq $dashboard_lib } @existing ) {
        $ENV{PERL5LIB} = join $path_sep, $dashboard_lib, @existing;
    }
}

my $cmd = shift @ARGV || '';
_prime_command_result_env( $cmd, @ARGV ) if $cmd ne '';

if ( $cmd eq '' ) {
    pod2usage(
        -exitval => 1,
        -verbose => 99,
        -sections => [ qw(NAME SYNOPSIS) ],
    );
}
elsif ( $cmd eq 'help' || $cmd eq '--help' || $cmd eq '-h' ) {
    pod2usage(
        -exitval => 0,
        -verbose => 99,
    );
}

# Handle query commands directly (don't try to exec them as files)
if ( $cmd && $cmd =~ /^(pjq|jq|pyq|yq|ptomq|tomq|pjp|propq|iniq|csvq|xmlq)$/ ) {
    require Developer::Dashboard::PathRegistry;
    my $paths = Developer::Dashboard::PathRegistry->new(
        workspace_roots => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
        project_roots   => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
    );
    Developer::Dashboard::InternalCLI::ensure_helpers( paths => $paths );
    require Developer::Dashboard::CLI::Query;
    Developer::Dashboard::CLI::Query::run_query_command( command => $cmd, args => \@ARGV );
    exit 0;
}

if ( $cmd eq 'of' || $cmd eq 'open-file' ) {
    require Developer::Dashboard::PathRegistry;
    require Developer::Dashboard::CLI::OpenFile;
    my $paths = Developer::Dashboard::PathRegistry->new(
        workspace_roots => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
        project_roots   => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
    );
    Developer::Dashboard::CLI::OpenFile::run_open_file_command( paths => $paths, args => \@ARGV );
    exit 0;
}

require Developer::Dashboard::Auth;
require Developer::Dashboard::ActionRunner;
require Developer::Dashboard::Codec;
Developer::Dashboard::Codec->import(qw(encode_payload decode_payload));
require Developer::Dashboard::CollectorRunner;
require Developer::Dashboard::Collector;
require Developer::Dashboard::Config;
require Developer::Dashboard;
require Developer::Dashboard::DockerCompose;
require Developer::Dashboard::Doctor;
require Developer::Dashboard::FileRegistry;
require Developer::Dashboard::IndicatorStore;
require Developer::Dashboard::JSON;
Developer::Dashboard::JSON->import(qw(json_encode));
require Developer::Dashboard::PageDocument;
require Developer::Dashboard::PageRuntime;
require Developer::Dashboard::PageResolver;
require Developer::Dashboard::PageStore;
require Developer::Dashboard::PathRegistry;
require Developer::Dashboard::Prompt;
require Developer::Dashboard::RuntimeManager;
require Developer::Dashboard::SessionStore;
require Developer::Dashboard::Web::App;
require Developer::Dashboard::Web::Server;

my $paths = Developer::Dashboard::PathRegistry->new(
    workspace_roots => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
    project_roots   => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
);
my $files      = Developer::Dashboard::FileRegistry->new( paths => $paths );
my $indicators = Developer::Dashboard::IndicatorStore->new( paths => $paths );
my $collectors = Developer::Dashboard::Collector->new( paths => $paths );
my $runner     = Developer::Dashboard::CollectorRunner->new(
    collectors => $collectors,
    files      => $files,
    indicators => $indicators,
    paths      => $paths,
);
my $config     = Developer::Dashboard::Config->new( files => $files, paths => $paths );
my $pages      = Developer::Dashboard::PageStore->new( paths => $paths );
my $page_runtime = Developer::Dashboard::PageRuntime->new(
    files   => $files,
    paths   => $paths,
    aliases => $config->path_aliases,
);
my $auth = Developer::Dashboard::Auth->new(
    files => $files,
    paths => $paths,
);
my $actions = Developer::Dashboard::ActionRunner->new(
    files => $files,
    paths => $paths,
);
my $resolver = Developer::Dashboard::PageResolver->new(
    actions => $actions,
    config  => $config,
    pages   => $pages,
    paths   => $paths,
);
my $docker = Developer::Dashboard::DockerCompose->new(
    config  => $config,
    paths   => $paths,
);
my $doctor = Developer::Dashboard::Doctor->new(
    paths => $paths,
);
my $prompt = Developer::Dashboard::Prompt->new(
    paths      => $paths,
    indicators => $indicators,
);
my $sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
my $runtime = Developer::Dashboard::RuntimeManager->new(
    app_builder => sub {
        my (%args) = @_;
        my $web_auth = Developer::Dashboard::Auth->new( files => $files, paths => $paths );
        my $web_pages = Developer::Dashboard::PageStore->new( paths => $paths );
        my $web_sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
        my $app = Developer::Dashboard::Web::App->new(
            actions  => $actions,
            auth     => $web_auth,
            config   => $config,
            pages    => $web_pages,
            prompt   => $prompt,
            runtime  => $page_runtime,
            resolver => $resolver,
            sessions => $web_sessions,
        );
        return Developer::Dashboard::Web::Server->new(
            app     => $app,
            host    => $args{host},
            port    => $args{port},
            workers => $args{workers},
            ssl     => $args{ssl},
        );
    },
    config => $config,
    files  => $files,
    paths  => $paths,
    runner => $runner,
);

my $CONFIG_PATH_ALIASES_LOADED  = 0;
my $SAVED_PAGES_MIGRATED        = 0;
my $COLLECTOR_INDICATORS_SYNCED = 0;

# _prime_ticket_ref_env()
# Seeds TICKET_REF from the active tmux session when the shell environment does
# not already provide it.
# Input: none.
# Output: true after best-effort environment priming.
sub _prime_ticket_ref_env {
    return 1 if defined $ENV{TICKET_REF} && $ENV{TICKET_REF} ne '';
    my $ticket = _tmux_ticket_ref();
    $ENV{TICKET_REF} = $ticket if defined $ticket && $ticket ne '';
    return 1;
}

# _tmux_ticket_ref()
# Reads TICKET_REF from tmux session environment when available.
# Input: none.
# Output: ticket string or undef when tmux does not expose one.
sub _tmux_ticket_ref {
    my ( $stdout, undef, $exit_code ) = capture {
        system 'tmux', 'show-environment', 'TICKET_REF';
        return $? >> 8;
    };
    return if $exit_code != 0;
    return if !defined $stdout || $stdout eq '';
    for my $line ( split /\n/, $stdout ) {
        next if !defined $line || $line eq '' || $line =~ /^-/;
        return $1 if $line =~ /^TICKET_REF=(.+)$/;
    }
    return;
}

sub _load_configured_path_aliases {
    return 1 if $CONFIG_PATH_ALIASES_LOADED;
    $paths->register_named_paths( $config->path_aliases );
    $CONFIG_PATH_ALIASES_LOADED = 1;
    return 1;
}

sub _migrate_saved_pages {
    return 1 if $SAVED_PAGES_MIGRATED;
    $pages->migrate_legacy_json_pages;
    $SAVED_PAGES_MIGRATED = 1;
    return 1;
}

sub _collector_jobs {
    return $config->collectors;
}

sub _sync_configured_collector_indicators {
    return 1 if $COLLECTOR_INDICATORS_SYNCED;
    $indicators->sync_collectors( _collector_jobs() );
    $COLLECTOR_INDICATORS_SYNCED = 1;
    return 1;
}

if ( $cmd eq 'ps1' ) {
    _sync_configured_collector_indicators();
    _prime_ticket_ref_env();
    my $jobs = 0;
    my $cwd  = cwd();
    my $mode = 'compact';
    my $color = 0;
    my $max_age = 300;
    GetOptionsFromArray(
        \@ARGV,
        'jobs=i' => \$jobs,
        'cwd=s'  => \$cwd,
        'mode=s' => \$mode,
        'color!' => \$color,
        'max-age=i' => \$max_age,
    );
    print $prompt->render( jobs => $jobs, cwd => $cwd, mode => $mode, color => $color, max_age => $max_age );
    exit 0;
}
elsif ( $cmd eq 'paths' ) {
    _load_configured_path_aliases();
    my %out = (
        home           => $paths->home,
        home_runtime_root => $paths->home_runtime_root,
        project_runtime_root => scalar $paths->project_runtime_root,
        runtime_root   => $paths->runtime_root,
        state_root     => $paths->state_root,
        cache_root     => $paths->cache_root,
        logs_root      => $paths->logs_root,
        dashboards_root=> $paths->dashboards_root,
        bookmarks_root => $paths->bookmarks_root,
        cli_root       => $paths->cli_root,
        collectors_root=> $paths->collectors_root,
        indicators_root=> $paths->indicators_root,
        config_root    => $paths->config_root,
        current_project_root => scalar $paths->current_project_root,
        %{ $paths->named_paths },
    );
    print json_encode(\%out);
    exit 0;
}
elsif ( $cmd eq 'path' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'resolve' ) {
        _load_configured_path_aliases();
        my $name = shift @ARGV || die "Usage: dashboard path resolve <name>\n";
        print $paths->resolve_dir($name), "\n";
        exit 0;
    }
    elsif ( $action eq 'locate' ) {
        my @found = $paths->locate_projects(@ARGV);
        print json_encode( \@found );
        exit 0;
    }
    elsif ( $action eq 'add' ) {
        my $name = shift @ARGV || die "Usage: dashboard path add <name> <path>\n";
        my $path = shift @ARGV || die "Usage: dashboard path add <name> <path>\n";
        my $saved = $config->save_global_path_alias( $name, $path );
        $paths->register_named_paths( { $name => $path } );
        $saved->{resolved} = $paths->resolve_dir($name);
        print json_encode($saved);
        exit 0;
    }
    elsif ( $action eq 'del' ) {
        my $name = shift @ARGV || die "Usage: dashboard path del <name>\n";
        my $deleted = $config->remove_global_path_alias($name);
        $paths->unregister_named_path($name);
        print json_encode($deleted);
        exit 0;
    }
    elsif ( $action eq 'project-root' ) {
        my $root = $paths->current_project_root;
        print defined $root ? "$root\n" : '';
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        _load_configured_path_aliases();
        my %out = (
            home         => $paths->home,
            home_runtime => $paths->home_runtime_root,
            project_runtime => scalar $paths->project_runtime_root,
            runtime      => $paths->runtime_root,
            state        => $paths->state_root,
            cache        => $paths->cache_root,
            logs         => $paths->logs_root,
            dashboards   => $paths->dashboards_root,
            bookmarks    => $paths->bookmarks_root,
            cli          => $paths->cli_root,
            config       => $paths->config_root,
            collectors   => $paths->collectors_root,
            indicators   => $paths->indicators_root,
            %{ $paths->named_paths },
        );
        print json_encode(\%out);
        exit 0;
    }
}
elsif ( $cmd eq 'encode' ) {
    local $/;
    my $text = <STDIN>;
    print encode_payload($text), "\n";
    exit 0;
}
elsif ( $cmd eq 'decode' ) {
    local $/;
    my $token = <STDIN>;
    $token =~ s/\s+$//;
    print decode_payload($token);
    exit 0;
}
elsif ( $cmd eq 'indicator' ) {
    _sync_configured_collector_indicators() if ( ( $ARGV[0] || '' ) eq 'list' );
    my $action = shift @ARGV || '';
    if ( $action eq 'set' ) {
        my ( $name, $label, $icon, $status ) = @ARGV;
        die "Usage: dashboard indicator set <name> <label> <icon> <status>\n" if !$status;
        my $item = $indicators->set_indicator(
            $name,
            label          => $label,
            icon           => $icon,
            status         => $status,
            priority       => 100,
            prompt_visible => 1,
        );
        print json_encode($item);
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $indicators->list_indicators ] );
        exit 0;
    }
    elsif ( $action eq 'refresh-core' ) {
        my $cwd = shift @ARGV || cwd();
        print json_encode( $indicators->refresh_core_indicators( cwd => $cwd ) );
        exit 0;
    }
}
elsif ( $cmd eq 'collector' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'write-result' ) {
        my ( $name, $exit_code ) = @ARGV;
        die "Usage: dashboard collector write-result <name> <exit_code>\n" if !defined $exit_code;
        local $/;
        my $stdout = <STDIN>;
        $collectors->write_result(
            $name,
            exit_code => $exit_code,
            stdout    => defined $stdout ? $stdout : '',
            stderr    => '',
        );
        exit 0;
    }
    elsif ( $action eq 'status' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector status <name>\n";
        print json_encode( $collectors->read_status($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $collectors->list_collectors ] );
        exit 0;
    }
    elsif ( $action eq 'job' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector job <name>\n";
        print json_encode( $collectors->read_job($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'output' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector output <name>\n";
        print json_encode( $collectors->read_output($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'inspect' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector inspect <name>\n";
        print json_encode( $collectors->inspect_collector($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'log' ) {
        print $files->read('collector_log') // '';
        exit 0;
    }
    elsif ( $action eq 'run' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector run <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        print json_encode( $runner->run_once($job) );
        exit 0;
    }
    elsif ( $action eq 'start' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector start <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        my $pid = $runner->start_loop($job);
        print "$pid\n";
        exit 0;
    }
    elsif ( $action eq 'stop' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector stop <name>\n";
        my $pid = $runner->stop_loop($name);
        print defined $pid ? "$pid\n" : '';
        exit 0;
    }
    elsif ( $action eq 'restart' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector restart <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        $runner->stop_loop($name);
        my $pid = $runner->start_loop($job);
        print "$pid\n";
        exit 0;
    }
}
elsif ( $cmd eq 'config' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'init' ) {
        my $file = $config->save_global(
            {
                collectors => [
                    {
                        name     => 'example.collector',
                        command  => "printf 'example collector output\\n'",
                        cwd      => 'home',
                        interval => 60,
                    },
                ],
            }
        );
        print "$file\n";
        exit 0;
    }
    elsif ( $action eq 'show' ) {
        _load_configured_path_aliases();
        print json_encode( $config->merged );
        exit 0;
    }
}
elsif ( $cmd eq 'auth' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'add-user' ) {
        my ( $username, $password ) = @ARGV;
        die "Usage: dashboard auth add-user <username> <password>\n" if !$username || !$password;
        print json_encode(
            $auth->add_user(
                username => $username,
                password => $password,
            )
        );
        exit 0;
    }
    elsif ( $action eq 'list-users' ) {
        print json_encode( [ $auth->list_users ] );
        exit 0;
    }
    elsif ( $action eq 'remove-user' ) {
        my $username = shift @ARGV || die "Usage: dashboard auth remove-user <username>\n";
        $auth->remove_user($username);
        print json_encode( { removed => $username } );
        exit 0;
    }
}
elsif ( $cmd eq 'init' ) {
    _migrate_saved_pages();
    my $migrated = [];
    my $config_file = $config->save_global(
        {
            collectors => [
                {
                    name     => 'example.collector',
                    command  => "printf 'example collector output\\n'",
                    cwd      => 'home',
                    interval => 60,
                },
            ],
        }
    );

    my @pages = $pages->list_saved_pages;
    _seed_init_page( $pages, \@pages, _welcome_page() );
    _seed_init_page( $pages, \@pages, _api_dashboard_page() );
    _seed_init_page( $pages, \@pages, _db_dashboard_page() );
    my $internal_cli = Developer::Dashboard::InternalCLI::ensure_helpers( paths => $paths );

    print json_encode(
        {
            config_file => $config_file,
            internal_cli => $internal_cli,
            runtime_root => $paths->runtime_root,
            migrated_pages => $migrated,
            pages        => [ $pages->list_saved_pages ],
        }
    );
    exit 0;
}
elsif ( $cmd eq 'version' ) {
    print $Developer::Dashboard::VERSION, "\n";
    exit 0;
}
elsif ( $cmd eq 'page' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $action = shift @ARGV || '';
    if ( $action eq 'new' ) {
        my $id = shift @ARGV || '';
        my $title = shift @ARGV || 'Untitled';
        my $page = Developer::Dashboard::PageDocument->new(
            id          => $id || undef,
            title       => $title,
            description => 'Project-neutral Developer Dashboard page',
            layout      => {
                body => "Replace this body with your own page content.",
            },
            state => {
                project => '',
            },
            actions => [
                { id => 'example', label => 'Example Action' },
            ],
        );
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'save' ) {
        my $id = shift @ARGV || die "Usage: dashboard page save <id>\n";
        local $/;
        my $source = <STDIN>;
        my $page = $source =~ /^\s*\{/
          ? Developer::Dashboard::PageDocument->from_json($source)
          : Developer::Dashboard::PageDocument->from_instruction($source);
        $page->{id} = $id;
        my $file = $pages->save_page($page);
        print "$file\n";
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $resolver->list_pages ] );
        exit 0;
    }
    elsif ( $action eq 'show' ) {
        my $id = shift @ARGV || die "Usage: dashboard page show <id>\n";
        my $page = $resolver->load_named_page($id);
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'encode' ) {
        my $id = shift @ARGV;
        my $page;
        if ($id) {
            $page = $pages->load_saved_page($id);
        }
        else {
            local $/;
            my $source = <STDIN>;
            $page = $source =~ /^\s*\{/
              ? Developer::Dashboard::PageDocument->from_json($source)
              : Developer::Dashboard::PageDocument->from_instruction($source);
        }
        print $pages->encode_page($page), "\n";
        exit 0;
    }
    elsif ( $action eq 'decode' ) {
        my $token = shift @ARGV || do {
            local $/;
            scalar <STDIN>;
        };
        $token =~ s/\s+$// if defined $token;
        my $page = $pages->load_transient_page($token);
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'urls' ) {
        my $id = shift @ARGV || die "Usage: dashboard page urls <id>\n";
        my $page = $pages->load_saved_page($id);
        print json_encode(
            {
                edit   => $pages->editable_url($page),
                render => $pages->render_url($page),
                source => $pages->source_url($page),
            }
        );
        exit 0;
    }
    elsif ( $action eq 'render' ) {
        my $source = shift @ARGV || '';
        my $page;
        if ($source) {
            if ( -f $source ) {
                open my $fh, '<', $source or die "Unable to read $source: $!";
                local $/;
                my $raw = <$fh>;
                $page = $raw =~ /^\s*\{/
                  ? Developer::Dashboard::PageDocument->from_json($raw)
                  : Developer::Dashboard::PageDocument->from_instruction($raw);
            }
            else {
                $page = $resolver->load_named_page($source);
            }
        }
        else {
            local $/;
            my $source = <STDIN>;
            $page = $source =~ /^\s*\{/
              ? Developer::Dashboard::PageDocument->from_json($source)
              : Developer::Dashboard::PageDocument->from_instruction($source);
        }
        $page->with_mode('render');
        print $page->render_html;
        exit 0;
    }
    elsif ( $action eq 'source' ) {
        my $source = shift @ARGV || die "Usage: dashboard page source <id|token>\n";
        my $page = eval { $resolver->load_named_page($source) };
        if ( !$page ) {
            die $@ if $source !~ /^[A-Za-z0-9+\/=]+$/;
            $page = $pages->load_transient_page($source);
        }
        $page->with_mode('source');
        print $page->canonical_instruction;
        exit 0;
    }
}
elsif ( $cmd eq 'action' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $sub = shift @ARGV || '';
    if ( $sub eq 'run' ) {
        my $page_id = shift @ARGV || die "Usage: dashboard action run <page_id> <action_id>\n";
        my $action_id = shift @ARGV || die "Usage: dashboard action run <page_id> <action_id>\n";
        my $page = $resolver->load_named_page($page_id);
        my ($action) = grep { ref($_) eq 'HASH' && ( $_->{id} || '' ) eq $action_id } @{ $page->as_hash->{actions} || [] };
        die "Unknown action '$action_id'\n" if !$action;
        print json_encode(
            $actions->run_page_action(
                action => $action,
                page   => $page,
                source => $page->{meta}{source_kind} || 'saved',
            )
        );
        exit 0;
    }
}
elsif ( $cmd eq 'docker' ) {
    _load_configured_path_aliases();
    my $sub = shift @ARGV || '';
    if ( $sub eq 'compose' ) {
        my @addons;
        my @modes;
        my @services;
        my $project_root = '';
        my $dry_run = 0;
        Getopt::Long::Configure(qw(pass_through no_getopt_compat no_auto_abbrev));
        GetOptionsFromArray(
            \@ARGV,
            'addon=s@'   => \@addons,
            'mode=s@'    => \@modes,
            'service=s@' => \@services,
            'project=s'  => \$project_root,
            'dry-run!'   => \$dry_run,
        );
        Getopt::Long::Configure(qw(no_pass_through getopt_compat auto_abbrev));
        my $result = $docker->resolve(
            addons       => \@addons,
            args         => \@ARGV,
            modes        => \@modes,
            services     => \@services,
            project_root => $project_root || undef,
        );
        if ($dry_run) {
            print json_encode($result);
            exit 0;
        }
        chdir $result->{project_root} or die "Unable to chdir to $result->{project_root}: $!";
        local @ENV{ keys %{ $result->{env} } } = values %{ $result->{env} } if %{ $result->{env} };
        exec @{ $result->{command} };
        die "Unable to exec docker compose: $!";
    }
}
elsif ( $cmd eq 'serve' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $action = @ARGV && $ARGV[0] !~ /^-/ ? shift @ARGV : '';
    if ( $action eq 'logs' ) {
        my $follow = 0;
        my $lines;
        GetOptionsFromArray(
            \@ARGV,
            'f'   => \$follow,
            'n=i' => \$lines,
        );
        print $runtime->web_log(
            follow => $follow,
            ( defined $lines ? ( lines => $lines ) : () ),
        );
        exit 0;
    }
    if ( $action eq 'workers' ) {
        my $workers = shift @ARGV;
        my $host = '0.0.0.0';
        my $port = 7890;
        GetOptionsFromArray(
            \@ARGV,
            'host=s' => \$host,
            'port=i' => \$port,
        );
        my $saved = $config->save_global_web_workers($workers);
        my $running = $runtime->running_web;
        my $pid;
        if ( !$running ) {
            $pid = $runtime->start_web(
                host    => $host,
                port    => $port,
                workers => $saved->{workers},
            );
        }
        print json_encode(
            {
                %{$saved},
                ( defined $pid ? ( pid => $pid ) : () ),
            }
        );
        exit 0;
    }
    unshift @ARGV, $action if defined $action && $action ne '';
    my $settings = $config->web_settings;
    my $host = $settings->{host};
    my $port = $settings->{port};
    my $workers = $settings->{workers};
    my $ssl = $settings->{ssl};
    my $foreground = 0;
    GetOptionsFromArray(
        \@ARGV,
        'host=s'      => \$host,
        'port=i'      => \$port,
        'workers=i'   => \$workers,
        'ssl!'        => \$ssl,
        'foreground!' => \$foreground,
    );
    $config->save_global_web_settings(
        host    => $host,
        port    => $port,
        workers => $workers,
        ssl     => $ssl,
    );
    my $result = $runtime->start_web(
        foreground => $foreground,
        host       => $host,
        port       => $port,
        workers    => $workers,
        ssl        => $ssl,
    );
    if (!$foreground) {
        print json_encode(
            {
                host    => $host,
                port    => $port,
                workers => $workers,
                ssl     => $ssl,
                pid     => $result,
            }
        );
    }
    exit 0;
}
elsif ( $cmd eq 'stop' ) {
    print json_encode( $runtime->stop_all );
    exit 0;
}
elsif ( $cmd eq 'restart' ) {
    _load_configured_path_aliases();
    my $settings = $config->web_settings;
    my $host = $settings->{host};
    my $port = $settings->{port};
    my $workers = $settings->{workers};
    my $ssl = $settings->{ssl};
    GetOptionsFromArray(
        \@ARGV,
        'host=s'    => \$host,
        'port=i'    => \$port,
        'workers=i' => \$workers,
        'ssl!'      => \$ssl,
    );
    print json_encode(
        $runtime->restart_all(
            host    => $host,
            port    => $port,
            workers => $workers,
            ssl     => $ssl,
        )
    );
    exit 0;
}
elsif ( $cmd eq 'shell' ) {
    my $shell = normalize_shell_name( shift @ARGV || native_shell_name() );
    print _shell_bootstrap($shell);
    exit 0;
}
elsif ( $cmd eq 'doctor' ) {
    my $fix = 0;
    GetOptionsFromArray(
        \@ARGV,
        'fix!' => \$fix,
    );
    print json_encode(
        $doctor->run(
            fix => $fix,
        )
    );
    exit 0;
}
elsif ( $cmd eq 'skills' ) {
    require Developer::Dashboard::SkillManager;
    my $skill_mgr = Developer::Dashboard::SkillManager->new();
    
    my $action = shift @ARGV || '';
    
    if ( $action eq 'install' ) {
        my $git_url = shift @ARGV || '';
        die "Usage: dashboard skills install <git-url>\n" if !$git_url;
        my $result = $skill_mgr->install($git_url);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'uninstall' ) {
        my $repo_name = shift @ARGV || '';
        die "Usage: dashboard skills uninstall <repo-name>\n" if !$repo_name;
        my $result = $skill_mgr->uninstall($repo_name);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'update' ) {
        my $repo_name = shift @ARGV || '';
        die "Usage: dashboard skills update <repo-name>\n" if !$repo_name;
        my $result = $skill_mgr->update($repo_name);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'list' ) {
        my $skills = $skill_mgr->list();
        print json_encode({ skills => $skills });
        exit 0;
    }
    else {
        die "Unknown skills action: $action\nUsage: dashboard skills [install|uninstall|update|list]\n";
    }
}
elsif ( $cmd eq 'skill' ) {
    require Developer::Dashboard::SkillDispatcher;
    my $dispatcher = Developer::Dashboard::SkillDispatcher->new();
    
    my $skill_name = shift @ARGV || '';
    my $skill_cmd = shift @ARGV || '';
    
    die "Usage: dashboard skill <skill-name> <command> [args...]\n" if !$skill_name || !$skill_cmd;
    
    my $result = $dispatcher->dispatch( $skill_name, $skill_cmd, @ARGV );
    
    if ( $result->{error} ) {
        print STDERR $result->{error}, "\n";
        exit 1;
    }
    
    print $result->{stdout} if $result->{stdout};
    print STDERR $result->{stderr} if $result->{stderr};
    exit( $result->{exit_code} || 0 );
}
else {
    my $user_cli = _custom_command_path($cmd);
    if ( -d $user_cli ) {
        my $run = _resolve_directory_runner($user_cli);
        if ($run) {
            my @command = command_argv_for_path($run);
            exec { $command[0] } @command, @ARGV;
            die "Unable to exec $run: $!";
        }
    }
    if ( my $command_path = resolve_runnable_file($user_cli) ) {
        my @command = command_argv_for_path($command_path);
        exec { $command[0] } @command, @ARGV;
        die "Unable to exec $command_path: $!";
    }
}

# _prime_command_result_env($cmd, @argv)
# Runs executable command hook files from ~/.developer-dashboard/cli/<cmd> and
# exports their captured outputs as RESULT JSON for later hooks and the final
# command implementation.
# Input: top-level command name plus the remaining argv list.
# Output: true when hook processing completes.
sub _prime_command_result_env {
    my ( $cmd, @argv ) = @_;
    delete $ENV{RESULT};
    $ENV{DEVELOPER_DASHBOARD_COMMAND} = $cmd if defined $cmd && $cmd ne '';
    return 1 if !defined $cmd || $cmd eq '';

    my $hook_root = _command_hook_root($cmd);
    return 1 if !-d $hook_root;
    my $stream = $cmd eq 'doctor' ? 0 : 1;

    opendir my $dh, $hook_root or return 1;
    my %results;
    for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir $dh ) {
        my $path = File::Spec->catfile( $hook_root, $entry );
        next if !is_runnable_file($path);
        next if $entry eq 'run';

        my $hook_result = _run_command_hook_streaming( $path, stream => $stream, argv => \@argv );
        $results{$entry} = {
            stdout => $hook_result->{stdout},
            stderr => $hook_result->{stderr},
        };
        $results{$entry}{exit_code} = $hook_result->{exit_code} if defined $hook_result->{exit_code};
        $ENV{RESULT} = JSON::XS->new->canonical->encode( \%results );
    }
    closedir $dh;

    delete $ENV{RESULT} if !%results;
    return 1;
}

# _run_command_hook_streaming($path, @argv)
# Runs one executable hook file, streams its stdout/stderr live, and captures
# both channels for RESULT JSON propagation.
# Input: executable path plus remaining dashboard argv list.
# Output: hash reference with stdout, stderr, and exit_code.
sub _run_command_hook_streaming {
    my ( $path, %args ) = @_;
    my $stream = exists $args{stream} ? $args{stream} : 1;
    my @argv = @{ $args{argv} || [] };
    open my $stdin, '<', File::Spec->devnull() or die "Unable to open " . File::Spec->devnull() . " for hook stdin: $!";
    my $stderr = gensym();
    my $stdout;
    my @command = command_argv_for_path($path);
    my $pid = open3( $stdin, $stdout, $stderr, @command, @argv );
    close $stdin;

    my $selector = IO::Select->new( $stdout, $stderr );
    my $stdout_fd = fileno($stdout);
    my $stderr_fd = fileno($stderr);
    my $stdout_text = '';
    my $stderr_text = '';
    local $| = 1;
    STDOUT->autoflush(1);
    STDERR->autoflush(1);

    while ( my @ready = $selector->can_read ) {
        for my $fh (@ready) {
            my $buffer = '';
            my $read = sysread( $fh, $buffer, 8192 );
            if ( !defined $read || $read == 0 ) {
                $selector->remove($fh);
                close $fh;
                next;
            }

            if ( fileno($fh) == $stdout_fd ) {
                print STDOUT $buffer if $stream;
                $stdout_text .= $buffer;
                next;
            }

            if ( fileno($fh) == $stderr_fd ) {
                print STDERR $buffer if $stream;
                $stderr_text .= $buffer;
                next;
            }
        }
    }

    waitpid( $pid, 0 );
    return {
        stdout    => $stdout_text,
        stderr    => $stderr_text,
        exit_code => $? >> 8,
    };
}

# _command_hook_root($cmd)
# Resolves the per-command hook directory under the runtime CLI extension root.
# Input: top-level command name string.
# Output: directory path string, preferring <command>/ over <command>.d/.
sub _command_hook_root {
    my ($cmd) = @_;
    return '' if !defined $cmd || $cmd eq '';
    for my $root ( _cli_runtime_roots() ) {
        my $plain_root = File::Spec->catdir( $root, $cmd );
        return $plain_root if -d $plain_root;
        my $d_root = File::Spec->catdir( $root, $cmd . '.d' );
        return $d_root if -d $d_root;
    }
    return File::Spec->catdir( $ENV{HOME}, '.developer-dashboard', 'cli', $cmd );
}

# _custom_command_path($cmd)
# Resolves the effective custom command path from project-local and home CLI roots.
# Input: top-level command name string.
# Output: file or directory path string.
sub _custom_command_path {
    my ($cmd) = @_;
    return '' if !defined $cmd || $cmd eq '';
    if ( my $helper = Developer::Dashboard::InternalCLI::canonical_helper_name($cmd) ) {
        require Developer::Dashboard::PathRegistry;
        my $paths = Developer::Dashboard::PathRegistry->new(
            home            => $ENV{HOME},
            workspace_roots => [],
            project_roots   => [],
        );
        Developer::Dashboard::InternalCLI::ensure_helpers( paths => $paths );
        return Developer::Dashboard::InternalCLI::helper_path( paths => $paths, name => $helper );
    }
    for my $root ( _cli_runtime_roots() ) {
        my $path = File::Spec->catfile( $root, $cmd );
        return $path if -e $path;
    }
    return File::Spec->catfile( $ENV{HOME}, '.developer-dashboard', 'cli', $cmd );
}

# _cli_runtime_roots()
# Returns project-local then home CLI roots for top-level command resolution before the heavy runtime loads.
# Input: none.
# Output: ordered list of CLI root directory path strings.
sub _cli_runtime_roots {
    my @roots;
    my %seen;
    my $cwd = cwd();
    my $project_root = _project_root_for($cwd);
    for my $root (
        ( defined $project_root && -d File::Spec->catdir( $project_root, '.developer-dashboard' )
            ? File::Spec->catdir( $project_root, '.developer-dashboard', 'cli' )
            : () ),
        File::Spec->catdir( $ENV{HOME}, '.developer-dashboard', 'cli' ),
      )
    {
        next if !defined $root || $root eq '';
        next if $seen{$root}++;
        push @roots, $root;
    }
    return @roots;
}

# _project_root_for($start_dir)
# Resolves the nearest project root containing a .git directory.
# Input: starting directory path string.
# Output: project root directory path string or undef.
sub _project_root_for {
    my ($dir) = @_;
    while ($dir) {
        return $dir if -d File::Spec->catdir( $dir, '.git' );
        my $parent = File::Spec->catdir( $dir, File::Spec->updir );
        $parent = Cwd::abs_path($parent) if $parent ne $dir;
        last if !$parent || $parent eq $dir;
        $dir = $parent;
    }
    return;
}

# _seed_init_page($pages, $known_ids, $page)
# Saves one seeded page when it does not already exist in the effective bookmark roots.
# Input: page store, array reference of known ids, and page document object.
# Output: true when the page already existed or was written.
sub _seed_init_page {
    my ( $pages, $known_ids, $page ) = @_;
    my $id = $page->as_hash->{id} || return 1;
    return 1 if grep { $_ eq $id } @{ $known_ids || [] };
    $pages->save_page($page);
    push @{ $known_ids || [] }, $id;
    return 1;
}

# _welcome_page()
# Builds the seeded welcome bookmark page for dashboard init.
# Input: none.
# Output: Developer::Dashboard::PageDocument object.
sub _welcome_page {
    return Developer::Dashboard::PageDocument->new(
        id          => 'welcome',
        title       => 'Welcome to Developer Dashboard',
        description => 'A project-neutral local dashboard starter page.',
        layout      => {
            body => "Developer Dashboard is ready.\n\nUse dashboard page new/save to create more pages.\nUse dashboard serve to browse them.\nUse dashboard collector run to refresh prepared data.\nUse dashboard ps1 from your shell to render prompt status.",
        },
        state => {
            project => '',
        },
        actions => [
            { id => 'serve', label => 'Run dashboard serve' },
            { id => 'ps1',   label => 'Use dashboard ps1 in your shell' },
        ],
    );
}

# _api_dashboard_page()
# Builds the seeded API dashboard bookmark page inspired by the legacy request workspace without carrying forward sensitive defaults.
# Input: none.
# Output: Developer::Dashboard::PageDocument object.
sub _api_dashboard_page {
    return Developer::Dashboard::PageDocument->from_instruction(<<'BOOKMARK');
TITLE: API Dashboard
:--------------------------------------------------------------------------------:
BOOKMARK: api-dashboard
:--------------------------------------------------------------------------------:
NOTE: A Postman-style request workspace with collection import/export, multiple request tabs, and bookmark-backed request dispatch.
:--------------------------------------------------------------------------------:
HTML: <script src="/js/jquery.js"></script>
<style>
.api-postman-shell { display:grid; gap:18px; }
.api-card { border:1px solid #d9d3c7; background:linear-gradient(180deg, #fffaf1 0%, #fffdf8 100%); border-radius:18px; box-shadow:0 10px 30px rgba(31,42,46,0.08); overflow:hidden; }
.api-card h2, .api-card h3 { margin:0; }
.api-sidebar-header, .api-main-toolbar { padding:16px 18px; border-bottom:1px solid #e6ddcf; background:#f7efe0; }
.api-sidebar-body, .api-main-body { padding:18px; }
.api-shell-tabs, .api-response-tabs, .api-collection-tabs, .api-tab-strip { display:flex; gap:6px; flex-wrap:wrap; align-items:flex-end; border-bottom:1px solid #d8ccb8; }
.api-shell-tabs { padding:0 12px; }
.api-response-tabs, .api-collection-tabs, .api-tab-strip { margin:0; }
.api-shell-tab, .api-response-tab, .api-collection-tab, .api-tab {
  display:inline-flex;
  align-items:center;
  gap:8px;
  border:1px solid #d1b783;
  border-bottom:0;
  border-radius:12px 12px 0 0;
  background:#efe3cd;
  color:#6a4512;
  cursor:pointer;
  font-weight:700;
  padding:10px 16px;
  margin:0 0 -1px;
}
.api-shell-tab[aria-selected="true"], .api-response-tab[aria-selected="true"], .api-collection-tab[aria-selected="true"], .api-tab.is-active {
  background:#fffdf8;
  border-color:#d8ccb8;
  color:#1f2a2e;
}
.api-shell-panel[hidden], .api-response-panel[hidden] { display:none !important; }
.api-toolbar-group { display:flex; gap:10px; flex-wrap:wrap; }
.api-toolbar-group button, .api-request-actions button { border:1px solid #d1b783; border-radius:999px; background:#fff7e7; color:#6a4512; cursor:pointer; font-weight:600; padding:9px 14px; }
.api-toolbar-group button:hover, .api-request-actions button:hover { background:#f7e1af; }
.api-toolbar-group input[type=file] { color:#6a4512; font-weight:600; max-width:280px; }
.api-toolbar-group input[type=file]::file-selector-button { border:1px solid #d1b783; border-radius:999px; background:#fff7e7; color:#6a4512; cursor:pointer; font-weight:600; padding:9px 14px; margin-right:10px; }
.api-toolbar-group input[type=file]::file-selector-button:hover { background:#f7e1af; }
.api-collection-tree { display:grid; gap:12px; }
.api-collection-card { border:1px solid #eadfca; border-radius:0 14px 14px 14px; padding:14px; background:#fffdf8; }
.api-collection-card.is-active { border-color:#0b7a75; box-shadow:0 0 0 2px rgba(11,122,117,0.10); }
.api-collection-card h3 { font-size:1rem; margin-bottom:8px; }
.api-node-list { list-style:none; margin:0; padding:0; display:grid; gap:6px; }
.api-node-list ul { list-style:none; margin:8px 0 0 14px; padding:0 0 0 10px; display:grid; gap:6px; border-left:1px dashed #d8ccb8; }
.api-node-button { width:100%; text-align:left; border:1px solid #e9dec8; border-radius:10px; background:#fff; padding:8px 10px; cursor:pointer; color:#1f2a2e; }
.api-node-button:hover { border-color:#0b7a75; color:#0b7a75; }
.api-node-button.is-active { border-color:#0b7a75; background:#e7f5f4; color:#0b7a75; font-weight:700; }
.api-node-folder { font-weight:700; background:#f8f1e4; }
.api-empty { color:#6a767b; font-style:italic; }
.api-location { margin:0 0 12px; padding:10px 12px; border:1px dashed #d8ccb8; border-radius:12px; background:#fffdf8; color:#6a4512; font-size:0.92rem; line-height:1.5; }
.api-tab-close { border:0; background:transparent; color:inherit; cursor:pointer; padding:0; font-size:1rem; line-height:1; }
.api-editor-grid { display:grid; gap:14px; grid-template-columns:repeat(2, minmax(0, 1fr)); }
.api-field { display:grid; gap:6px; }
.api-field label { font-weight:700; color:#6a4512; }
.api-field input, .api-field select, .api-field textarea { width:100%; box-sizing:border-box; padding:10px 12px; border:1px solid #d8ccb8; border-radius:12px; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:0.95rem; background:#fffefb; }
.api-field textarea { min-height:140px; resize:vertical; }
.api-field-full { grid-column:1 / -1; }
.api-token-shell { border:1px solid #e6ddcf; border-radius:14px; padding:12px; background:#fffaf1; }
.api-token-help { margin:0; color:#6a767b; font-size:0.9rem; }
.api-token-fields { display:grid; gap:10px; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); margin-top:12px; }
.api-token-field { display:grid; gap:6px; }
.api-token-field input { width:100%; box-sizing:border-box; padding:10px 12px; border:1px solid #d8ccb8; border-radius:12px; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:0.95rem; background:#fffefb; }
.api-inline-toggles { display:flex; gap:14px; flex-wrap:wrap; align-items:center; }
.api-inline-toggles label { font-weight:600; color:#1f2a2e; display:inline-flex; gap:6px; align-items:center; }
.api-request-actions { display:flex; gap:10px; flex-wrap:wrap; }
.api-response-shell { display:grid; gap:0; }
.api-response-panel { padding:0; }
.api-response-box { border:1px solid #d8ccb8; border-radius:14px 14px 0 0; overflow:hidden; background:#fffdf8; }
.api-response-box header { padding:10px 14px; background:#f7efe0; font-weight:700; color:#6a4512; border-bottom:1px solid #e6ddcf; }
.api-response-box pre { margin:0; padding:14px; min-height:220px; white-space:pre-wrap; overflow:auto; background:#1f2a2e; color:#f4efe3; }
.api-response-preview { display:none; padding:14px; min-height:220px; background:#f4f1eb; border-top:1px solid #e6ddcf; }
.api-response-preview.is-visible { display:block; }
.api-response-preview img, .api-response-preview iframe, .api-response-preview object { width:100%; max-width:100%; min-height:220px; border:0; border-radius:12px; background:#fff; }
.api-response-meta { display:flex; gap:10px; flex-wrap:wrap; }
.api-chip { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#e6f4f3; color:#0b7a75; font-weight:700; }
.api-chip.is-error { background:#fde9e9; color:#9f2020; }
.api-banner { border:1px solid #d1b783; border-radius:14px; background:#fff6e2; color:#6a4512; padding:12px 14px; display:none; margin-bottom:12px; }
@media (max-width: 980px) {
  .api-editor-grid { grid-template-columns:1fr; }
}
</style>
<div class="api-postman-shell">
  <div class="api-shell-tabs" role="tablist" aria-label="API Dashboard Sections">
    <button type="button" id="api-shell-tab-collections" class="api-shell-tab" role="tab" aria-selected="false" aria-controls="api-shell-panel-collections" data-api-shell-tab="collections">Collections</button>
    <button type="button" id="api-shell-tab-workspace" class="api-shell-tab" role="tab" aria-selected="false" aria-controls="api-shell-panel-workspace" data-api-shell-tab="workspace">Workspace</button>
  </div>
  <aside class="api-card api-shell-panel" id="api-shell-panel-collections" role="tabpanel" aria-labelledby="api-shell-tab-collections" data-api-shell-panel="collections">
    <div class="api-sidebar-header">
      <h2>Collections</h2>
      <p style="margin:8px 0 0;color:#6a767b">Import and export Postman collection JSON, then open requests in local tabs.</p>
    </div>
    <div class="api-sidebar-body">
      <div class="api-toolbar-group" style="margin-bottom:14px">
        <button type="button" id="api-new-collection">New Collection</button>
        <button type="button" id="api-rename-collection">Rename Collection</button>
        <button type="button" id="api-delete-collection">Delete Collection</button>
        <button type="button" id="api-export-collection">Export Postman Collection</button>
        <input id="api-import-file" type="file" accept=".json,application/json" aria-label="Import Postman Collection">
      </div>
      <div id="api-banner" class="api-banner"></div>
      <div id="api-collection-location" class="api-location">Current Location: Workspace root</div>
      <div id="api-collection-tree" class="api-collection-tree"></div>
    </div>
  </aside>
  <section class="api-card api-shell-panel" id="api-shell-panel-workspace" role="tabpanel" aria-labelledby="api-shell-tab-workspace" data-api-shell-panel="workspace">
    <div class="api-main-toolbar">
      <h2>Workspace</h2>
      <p style="margin:8px 0 0;color:#6a767b">Use multiple tabs to compare requests, save useful calls into collections, and dispatch through the bookmark Ajax sender.</p>
    </div>
    <div class="api-main-body">
      <div class="api-toolbar-group">
        <button type="button" id="api-new-tab">New Tab</button>
        <button type="button" id="api-duplicate-tab">Duplicate Tab</button>
        <button type="button" id="api-save-request">Save Request To Collection</button>
        <button type="button" id="api-delete-request">Delete Saved Request</button>
      </div>
      <div id="api-tab-strip" class="api-tab-strip"></div>
      <div class="api-editor-grid">
        <div class="api-field">
          <label for="api-request-name">Request Name</label>
          <input id="api-request-name" placeholder="List Orders">
        </div>
        <div class="api-field">
          <label for="api-collection-name">Selected Collection</label>
          <input id="api-collection-name" placeholder="Scratch Pad" readonly>
        </div>
        <div class="api-field">
          <label for="api-request-method">Method</label>
          <select id="api-request-method">
            <option>GET</option>
            <option>POST</option>
            <option>PUT</option>
            <option>PATCH</option>
            <option>DELETE</option>
            <option>HEAD</option>
            <option>OPTIONS</option>
          </select>
        </div>
        <div class="api-field">
          <label for="api-timeout">Timeout (seconds)</label>
          <input id="api-timeout" type="number" min="1" max="600" value="120">
        </div>
        <div class="api-field api-field-full api-token-shell" id="api-token-shell" hidden>
          <label for="api-token-fields">Request Token Values</label>
          <p class="api-token-help">Fill placeholders used by the selected request. Values are shared across the active collection and carry over to matching tokens in other requests from the same collection.</p>
          <div id="api-token-fields" class="api-token-fields"></div>
        </div>
        <div class="api-field api-field-full">
          <label for="api-request-url">URL</label>
          <input id="api-request-url" placeholder="https://example.test/api/orders">
        </div>
        <div class="api-field api-field-full">
          <label for="api-request-variables">Collection Variables</label>
          <textarea id="api-request-variables" placeholder="base_url=https://example.test&#10;token=abc123"></textarea>
        </div>
        <div class="api-field api-field-full">
          <label for="api-request-headers">Headers</label>
          <textarea id="api-request-headers" placeholder="Accept: application/json&#10;Authorization: Bearer {{token}}"></textarea>
        </div>
        <div class="api-field api-field-full">
          <label for="api-request-body">Body</label>
          <textarea id="api-request-body" placeholder='{"status":"open"}'></textarea>
        </div>
        <div class="api-field api-field-full">
          <label for="api-request-description">Description</label>
          <textarea id="api-request-description" placeholder="Request notes or Postman collection description."></textarea>
        </div>
        <div class="api-field api-field-full">
          <div class="api-inline-toggles">
            <label><input id="api-follow-redirects" type="checkbox" checked> Follow redirects</label>
            <label><input id="api-insecure-tls" type="checkbox"> Allow insecure TLS</label>
          </div>
        </div>
      </div>
      <div class="api-request-actions">
        <button type="button" id="api-send-request">Send Request</button>
        <button type="button" id="api-clear-response">Clear Response</button>
      </div>
      <div id="api-response-meta" class="api-response-meta"></div>
      <div class="api-response-shell">
        <section class="api-response-box">
          <section class="api-response-panel" id="api-response-panel-request" role="tabpanel" aria-labelledby="api-response-tab-request" data-api-response-panel="request">
            <header>Request Details</header>
            <pre id="api-request-details">Ready.</pre>
          </section>
          <section class="api-response-panel" id="api-response-panel-body" role="tabpanel" aria-labelledby="api-response-tab-body" data-api-response-panel="body">
            <header>Response Body</header>
            <div id="api-response-preview" class="api-response-preview"></div>
            <pre id="api-response-body">Ready.</pre>
          </section>
          <section class="api-response-panel" id="api-response-panel-headers" role="tabpanel" aria-labelledby="api-response-tab-headers" data-api-response-panel="headers">
            <header>Response Headers</header>
            <pre id="api-response-headers">Ready.</pre>
          </section>
        </section>
        <div class="api-response-tabs" role="tablist" aria-label="API Response Sections">
          <button type="button" id="api-response-tab-request" class="api-response-tab" role="tab" aria-selected="false" aria-controls="api-response-panel-request" data-api-response-tab="request">Request Details</button>
          <button type="button" id="api-response-tab-body" class="api-response-tab" role="tab" aria-selected="false" aria-controls="api-response-panel-body" data-api-response-tab="body">Response Body</button>
          <button type="button" id="api-response-tab-headers" class="api-response-tab" role="tab" aria-selected="false" aria-controls="api-response-panel-headers" data-api-response-tab="headers">Response Headers</button>
        </div>
      </div>
    </div>
  </section>
</div>
<script>
var configs = {};
(function () {
  var STORAGE_KEY = 'developer-dashboard:api-dashboard:v6';
  var POSTMAN_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
  var state = null;

  // uid(prefix)
  // Purpose: create stable-looking local identifiers for collections, tabs, and requests.
  // Input: optional string prefix.
  // Output: string identifier.
  function uid(prefix) {
    return [prefix || 'id', Date.now().toString(36), Math.random().toString(36).slice(2, 10)].join('-');
  }

  // clone(value)
  // Purpose: deep-clone plain JSON-safe state objects.
  // Input: JSON-safe value.
  // Output: cloned value.
  function clone(value) {
    return JSON.parse(JSON.stringify(value));
  }

  // showBanner(message, isError)
  // Purpose: render explicit success or error banners for workspace actions.
  // Input: message string and boolean error flag.
  // Output: none.
  function showBanner(message, isError) {
    var banner = document.getElementById('api-banner');
    banner.textContent = message || '';
    banner.style.display = message ? 'block' : 'none';
    banner.style.borderColor = isError ? '#e3b0b0' : '#d1b783';
    banner.style.background = isError ? '#fff0f0' : '#fff6e2';
    banner.style.color = isError ? '#9f2020' : '#6a4512';
  }

  // defaultRequest(name)
  // Purpose: build a new empty request model for a tab or collection item.
  // Input: optional request name.
  // Output: request hash.
  function defaultRequest(name) {
    return {
      name: name || 'New Request',
      method: 'GET',
      url: 'https://example.test/api',
      headers_text: 'Accept: application/json',
      body: '',
      description: '',
      timeout_s: 120,
      insecure_tls: false,
      follow_redirects: true
    };
  }

  // scratchCollection()
  // Purpose: seed a safe local collection when no saved collections exist.
  // Input: none.
  // Output: collection hash.
  function scratchCollection() {
    return {
      id: uid('collection'),
      name: 'Scratch Pad',
      description: 'A local scratch collection for ad-hoc API calls.',
      variable: [
        { key: 'base_url', value: 'https://example.test' },
        { key: 'token', value: '' }
      ],
      item: [
        {
          id: uid('request'),
          kind: 'request',
          name: 'Example GET',
          request: defaultRequest('Example GET')
        }
      ]
    };
  }

  // normalizeCollectionVariable(variable)
  // Purpose: sanitize one collection variable entry.
  // Input: candidate variable object.
  // Output: normalized variable hash or null.
  function normalizeCollectionVariable(variable) {
    if (!variable || typeof variable !== 'object') return null;
    var key = String(variable.key || '').trim();
    if (!key) return null;
    return {
      key: key,
      value: variable.value == null ? '' : String(variable.value)
    };
  }

  // normalizeResponse(input)
  // Purpose: normalize per-tab response state loaded from local storage.
  // Input: candidate response payload.
  // Output: normalized response hash or null.
  function normalizeResponse(input) {
    if (!input || typeof input !== 'object') return null;
    var response = input.response && typeof input.response === 'object' ? input.response : {};
    var request = input.request && typeof input.request === 'object' ? input.request : {};
    return {
      ok: !!input.ok,
      error: input.error ? String(input.error) : '',
      request: {
        method: request.method ? String(request.method) : '',
        url: request.url ? String(request.url) : '',
        headers_text: request.headers_text ? String(request.headers_text) : '',
        body: request.body ? String(request.body) : '',
        timeout_s: request.timeout_s || 120,
        follow_redirects: request.follow_redirects !== false,
        insecure_tls: !!request.insecure_tls
      },
      response: {
        status: response.status || 0,
        reason: response.reason ? String(response.reason) : '',
        elapsed_ms: response.elapsed_ms || 0,
        content_type: response.content_type ? String(response.content_type) : '',
        headers: Array.isArray(response.headers) ? response.headers : [],
        body: response.body ? String(response.body) : '',
        body_mode: response.body_mode ? String(response.body_mode) : 'text',
        preview_media_type: response.preview_media_type ? String(response.preview_media_type) : '',
        preview_url: response.preview_url ? String(response.preview_url) : '',
        body_size_bytes: response.body_size_bytes || 0
      }
    };
  }

  // normalizeRequestData(input)
  // Purpose: sanitize request settings loaded from collections, tabs, or imports.
  // Input: candidate request hash.
  // Output: normalized request hash.
  function normalizeRequestData(input) {
    var request = input && typeof input === 'object' ? input : {};
    var data = defaultRequest(request.name || 'New Request');
    if (request.method) data.method = String(request.method).toUpperCase();
    if (request.url) data.url = String(request.url);
    if (request.headers_text) data.headers_text = String(request.headers_text);
    if (request.body) data.body = String(request.body);
    if (request.description) data.description = String(request.description);
    if (request.timeout_s) data.timeout_s = request.timeout_s;
    if (request.insecure_tls) data.insecure_tls = true;
    if (request.follow_redirects === false) data.follow_redirects = false;
    return data;
  }

  // normalizeNode(node)
  // Purpose: sanitize a collection tree node.
  // Input: candidate folder or request node.
  // Output: normalized node hash or null.
  function normalizeNode(node) {
    if (!node || typeof node !== 'object') return null;
    if (Array.isArray(node.item)) {
      return {
        id: node.id || uid('folder'),
        kind: 'folder',
        name: node.name || 'Folder',
        item: node.item.map(normalizeNode).filter(Boolean)
      };
    }
    var request = normalizeRequestData(node.request || node);
    return {
      id: node.id || uid('request'),
      kind: 'request',
      name: node.name || request.name || 'Request',
      request: request
    };
  }

  // normalizeCollection(collection)
  // Purpose: sanitize a Postman-style collection object for local use.
  // Input: candidate collection hash.
  // Output: normalized collection hash or null.
  function normalizeCollection(collection) {
    if (!collection || typeof collection !== 'object') return null;
    return {
      id: collection.id || uid('collection'),
      name: collection.name || (collection.info && collection.info.name) || 'Collection',
      description: collection.description || (collection.info && collection.info.description) || '',
      variable: (collection.variable || []).map(normalizeCollectionVariable).filter(Boolean),
      item: (collection.item || []).map(normalizeNode).filter(Boolean)
    };
  }

  // normalizeTab(tab)
  // Purpose: sanitize one workspace tab and its last response.
  // Input: candidate tab hash.
  // Output: normalized tab hash or null.
  function normalizeTab(tab) {
    if (!tab || typeof tab !== 'object') return null;
    return {
      id: tab.id || uid('tab'),
      title: tab.title || (tab.request && tab.request.name) || 'New Tab',
      collection_id: tab.collection_id || '',
      request_id: tab.request_id || '',
      request: normalizeRequestData(tab.request || {}),
      response: normalizeResponse(tab.response || null)
    };
  }

  // flattenRequests(items, out)
  // Purpose: flatten a nested collection tree into request nodes only.
  // Input: item array and output array.
  // Output: output array.
  function flattenRequests(items, out) {
    (items || []).forEach(function (item) {
      if (item.kind === 'folder') {
        flattenRequests(item.item || [], out);
        return;
      }
      out.push(item);
    });
    return out;
  }

  // findFirstRequest(collection)
  // Purpose: choose a default request node from a collection.
  // Input: normalized collection hash.
  // Output: request node hash.
  function findFirstRequest(collection) {
    var requests = flattenRequests(collection.item || [], []);
    return requests[0] || {
      id: uid('request'),
      kind: 'request',
      name: 'New Request',
      request: defaultRequest('New Request')
    };
  }

  // tabFromRequest(node, collectionId)
  // Purpose: open a request node inside a new workspace tab.
  // Input: request node and owning collection id.
  // Output: tab hash.
  function tabFromRequest(node, collectionId) {
    return {
      id: uid('tab'),
      title: node.name || (node.request && node.request.name) || 'Request',
      collection_id: collectionId || '',
      request_id: node.id || '',
      request: normalizeRequestData(node.request || node),
      response: null
    };
  }

  // normalizeState(input)
  // Purpose: sanitize the persisted workspace state.
  // Input: candidate state hash.
  // Output: normalized workspace state.
  function normalizeState(input) {
    var data = input && typeof input === 'object' ? input : {};
    var collections = Array.isArray(data.collections) ? data.collections.map(normalizeCollection).filter(Boolean) : [];
    var tabs = Array.isArray(data.tabs) ? data.tabs.map(normalizeTab).filter(Boolean) : [];
    if (!collections.length) collections = [scratchCollection()];
    if (!tabs.length) tabs = [tabFromRequest(findFirstRequest(collections[0]), collections[0].id)];
    return {
      collections: collections,
      tabs: tabs,
      active_tab_id: data.active_tab_id || tabs[0].id,
      active_shell_tab: data.active_shell_tab === 'collections' ? 'collections' : 'workspace',
      active_response_tab: data.active_response_tab === 'request' || data.active_response_tab === 'headers' ? data.active_response_tab : 'body',
      selected_collection_id: data.selected_collection_id || collections[0].id,
      selected_request_id: data.selected_request_id || (tabs[0].request_id || '')
    };
  }

  // loadState()
  // Purpose: load local workspace state from browser storage.
  // Input: none.
  // Output: normalized workspace state.
  function loadState() {
    try {
      var raw = window.localStorage.getItem(STORAGE_KEY);
      if (!raw) return normalizeState({});
      return normalizeState(JSON.parse(raw));
    } catch (error) {
      showBanner('Unable to read saved API dashboard state: ' + error.message, true);
      return normalizeState({});
    }
  }

  // saveState()
  // Purpose: persist the current workspace state to local storage.
  // Input: none.
  // Output: none.
  function saveState() {
    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  }

  // setShellTab(name)
  // Purpose: activate one top-level shell tab and persist the preference.
  // Input: tab name string.
  // Output: none.
  function setShellTab(name) {
    state.active_shell_tab = name === 'collections' ? 'collections' : 'workspace';
    saveState();
  }

  // setResponseTab(name)
  // Purpose: activate one response panel tab and persist the preference.
  // Input: tab name string.
  // Output: none.
  function setResponseTab(name) {
    if (name !== 'request' && name !== 'headers') name = 'body';
    state.active_response_tab = name;
    saveState();
  }

  // findCollection(collectionId)
  // Purpose: resolve a collection by id from the current state.
  // Input: collection id string.
  // Output: collection hash or null.
  function findCollection(collectionId) {
    return (state.collections || []).find(function (collection) {
      return collection.id === collectionId;
    }) || null;
  }

  // findCollectionByName(name)
  // Purpose: resolve a collection by name from the current state.
  // Input: collection name string.
  // Output: collection hash or null.
  function findCollectionByName(name) {
    return (state.collections || []).find(function (collection) {
      return collection.name === name;
    }) || null;
  }

  // collectionNameExists(name, exceptCollectionId)
  // Purpose: detect collection name conflicts before saving or renaming.
  // Input: collection name string and optional collection id to exclude.
  // Output: boolean.
  function collectionNameExists(name, exceptCollectionId) {
    var trimmed = String(name || '').trim();
    return (state.collections || []).some(function (collection) {
      if (exceptCollectionId && collection.id === exceptCollectionId) return false;
      return collection.name === trimmed;
    });
  }

  // findNode(items, nodeId)
  // Purpose: resolve a request or folder node by id from a nested tree.
  // Input: items array and node id string.
  // Output: node hash or null.
  function findNode(items, nodeId) {
    for (var i = 0; i < (items || []).length; i++) {
      var item = items[i];
      if (item.id === nodeId) return item;
      if (item.kind === 'folder') {
        var found = findNode(item.item || [], nodeId);
        if (found) return found;
      }
    }
    return null;
  }

  // findRequestAcrossCollections(nodeId)
  // Purpose: locate a request node anywhere in the workspace by id.
  // Input: request node id string.
  // Output: object with collection and node keys, or null.
  function findRequestAcrossCollections(nodeId) {
    var i;
    for (i = 0; i < (state.collections || []).length; i++) {
      var collection = state.collections[i];
      var node = findNode(collection.item || [], nodeId);
      if (node && node.kind === 'request') {
        return {
          collection: collection,
          node: node
        };
      }
    }
    return null;
  }

  // findTab(tabId)
  // Purpose: resolve a tab by id from the current state.
  // Input: tab id string.
  // Output: tab hash or null.
  function findTab(tabId) {
    return (state.tabs || []).find(function (tab) {
      return tab.id === tabId;
    }) || null;
  }

  // findTabForRequest(collectionId, requestId)
  // Purpose: locate an already-open tab for a saved request.
  // Input: collection id and request id strings.
  // Output: tab hash or null.
  function findTabForRequest(collectionId, requestId) {
    return (state.tabs || []).find(function (tab) {
      return tab.collection_id === collectionId && tab.request_id === requestId;
    }) || null;
  }

  // adoptNodeIds(previousItems, incomingItems)
  // Purpose: preserve browser-local node ids across bootstrap refreshes when names and ordering still match.
  // Input: previous node array and incoming node array.
  // Output: none.
  function adoptNodeIds(previousItems, incomingItems) {
    var oldItems = previousItems || [];
    (incomingItems || []).forEach(function (item, index) {
      var previous = oldItems[index];
      if (!previous || previous.kind !== item.kind || previous.name !== item.name) return;
      item.id = previous.id;
      if (item.kind === 'folder') adoptNodeIds(previous.item || [], item.item || []);
    });
  }

  // adoptCollectionIds(previousCollection, incomingCollection)
  // Purpose: preserve browser-local collection and request ids when the same collection reloads from disk.
  // Input: previous collection hash and incoming collection hash.
  // Output: none.
  function adoptCollectionIds(previousCollection, incomingCollection) {
    if (!previousCollection || !incomingCollection) return;
    incomingCollection.id = previousCollection.id;
    adoptNodeIds(previousCollection.item || [], incomingCollection.item || []);
  }

  // activeTab()
  // Purpose: resolve the currently active tab.
  // Input: none.
  // Output: tab hash or null.
  function activeTab() {
    return (state.tabs || []).find(function (tab) {
      return tab.id === state.active_tab_id;
    }) || null;
  }

  // ensureActiveTab()
  // Purpose: guarantee the state points at an existing tab.
  // Input: none.
  // Output: none.
  function ensureActiveTab() {
    if (!activeTab() && state.tabs.length) state.active_tab_id = state.tabs[0].id;
  }

  // formatVariables(variable)
  // Purpose: convert collection variables to editable textarea text.
  // Input: variable array.
  // Output: string.
  function formatVariables(variable) {
    return (variable || []).map(function (entry) {
      return entry.key + '=' + (entry.value == null ? '' : entry.value);
    }).join('\n');
  }

  // parseVariablesText(text)
  // Purpose: parse editable textarea text into collection variables.
  // Input: textarea string.
  // Output: normalized variable array.
  function parseVariablesText(text) {
    return String(text || '').split(/\n/).map(function (line) {
      var trimmed = line.trim();
      if (!trimmed) return null;
      var pos = trimmed.indexOf('=');
      if (pos < 1) return null;
      return normalizeCollectionVariable({
        key: trimmed.slice(0, pos),
        value: trimmed.slice(pos + 1)
      });
    }).filter(Boolean);
  }

  // variableMap(collection)
  // Purpose: build a lookup hash for collection variables.
  // Input: collection hash.
  // Output: object map.
  function variableMap(collection) {
    var hash = {};
    (collection && collection.variable || []).forEach(function (entry) {
      hash[entry.key] = entry.value == null ? '' : String(entry.value);
    });
    return hash;
  }

  // applyVariables(collection, text)
  // Purpose: expand {{token}} placeholders from the selected collection variables.
  // Input: collection hash and source string.
  // Output: expanded string.
  function applyVariables(collection, text) {
    var value = String(text == null ? '' : text);
    var hash = variableMap(collection);
    return value.replace(/\{\{([^{}]+)\}\}/g, function (match, token) {
      var key = String(token || '').trim();
      return Object.prototype.hasOwnProperty.call(hash, key) ? hash[key] : match;
    });
  }

  // requestTokenNames(request)
  // Purpose: extract unique {{token}} placeholders used by one request template.
  // Input: normalized request hash.
  // Output: sorted array of token-name strings.
  function requestTokenNames(request) {
    var joined = [
      request && request.url || '',
      request && request.headers_text || '',
      request && request.body || ''
    ].join('\n');
    var found = {};
    joined.replace(/\{\{([^{}]+)\}\}/g, function (_, token) {
      var key = String(token || '').trim();
      if (key) found[key] = 1;
      return _;
    });
    return Object.keys(found).sort();
  }

  // upsertCollectionVariable(collection, key, value)
  // Purpose: create or update one collection variable entry in place.
  // Input: collection hash, variable key string, and variable value string.
  // Output: none.
  function upsertCollectionVariable(collection, key, value) {
    if (!collection) return;
    var entry = (collection.variable || []).find(function (item) {
      return item.key === key;
    });
    if (entry) {
      entry.value = value;
      return;
    }
    collection.variable = collection.variable || [];
    collection.variable.push({
      key: key,
      value: value
    });
  }

  // ensureRequestTokens(collection, request)
  // Purpose: make sure the active collection has variable entries for every token used by the active request.
  // Input: collection hash and normalized request hash.
  // Output: sorted token-name array.
  function ensureRequestTokens(collection, request) {
    var names = requestTokenNames(request);
    names.forEach(function (name) {
      upsertCollectionVariable(collection, name, variableMap(collection)[name] || '');
    });
    return names;
  }

  // setResolvedFieldValue(elementId, rawValue, resolvedValue)
  // Purpose: populate one request editor field with resolved text while preserving the raw template text in a data attribute.
  // Input: DOM element id string, raw template string, and resolved display string.
  // Output: none.
  function setResolvedFieldValue(elementId, rawValue, resolvedValue) {
    var element = document.getElementById(elementId);
    if (!element) return;
    element.dataset.apiRawValue = rawValue == null ? '' : String(rawValue);
    element.value = resolvedValue == null ? '' : String(resolvedValue);
  }

  // readResolvedFieldValue(elementId, collection)
  // Purpose: recover the raw template value for one editor field unless the user has changed the resolved display text.
  // Input: DOM element id string and collection hash for token expansion.
  // Output: raw template or current edited string.
  function readResolvedFieldValue(elementId, collection) {
    var element = document.getElementById(elementId);
    if (!element) return '';
    var raw = element.dataset.apiRawValue || '';
    var resolved = applyVariables(collection, raw);
    return element.value === resolved ? raw : element.value;
  }

  // renderResolvedRequestFields(collection, tab)
  // Purpose: repaint the request URL, headers, and body editors with collection-token expansion while preserving the raw templates.
  // Input: collection hash and active tab hash.
  // Output: none.
  function renderResolvedRequestFields(collection, tab) {
    setResolvedFieldValue('api-request-url', tab.request.url || '', applyVariables(collection, tab.request.url || ''));
    setResolvedFieldValue('api-request-headers', tab.request.headers_text || '', applyVariables(collection, tab.request.headers_text || ''));
    setResolvedFieldValue('api-request-body', tab.request.body || '', applyVariables(collection, tab.request.body || ''));
  }

  // renderTokenFields(collection, tab)
  // Purpose: render the request-specific token input form above the editor and keep the resolved request fields in sync.
  // Input: collection hash and active tab hash.
  // Output: none.
  function renderTokenFields(collection, tab) {
    var shell = document.getElementById('api-token-shell');
    var fields = document.getElementById('api-token-fields');
    fields.innerHTML = '';
    if (!collection || !tab) {
      shell.hidden = true;
      return;
    }
    var names = ensureRequestTokens(collection, tab.request);
    if (!names.length) {
      shell.hidden = true;
      return;
    }
    shell.hidden = false;
    names.forEach(function (name) {
      var field = document.createElement('div');
      field.className = 'api-token-field';
      var label = document.createElement('label');
      label.textContent = name;
      label.setAttribute('for', 'api-token-input-' + name.replace(/[^A-Za-z0-9_-]+/g, '-'));
      var input = document.createElement('input');
      input.id = 'api-token-input-' + name.replace(/[^A-Za-z0-9_-]+/g, '-');
      input.type = 'text';
      input.value = variableMap(collection)[name] || '';
      input.setAttribute('data-api-token-input', name);
      input.placeholder = '{{' + name + '}}';
      input.addEventListener('input', function () {
        upsertCollectionVariable(collection, name, input.value);
        document.getElementById('api-request-variables').value = formatVariables(collection.variable || []);
        renderResolvedRequestFields(collection, activeTab());
        saveState();
      });
      input.addEventListener('change', function () {
        upsertCollectionVariable(collection, name, input.value);
        document.getElementById('api-request-variables').value = formatVariables(collection.variable || []);
        persistWorkspace(true);
      });
      field.appendChild(label);
      field.appendChild(input);
      fields.appendChild(field);
    });
  }

  // responseChip(text, isError)
  // Purpose: build a response metadata badge.
  // Input: label text and boolean error flag.
  // Output: span element.
  function responseChip(text, isError) {
    var span = document.createElement('span');
    span.className = 'api-chip' + (isError ? ' is-error' : '');
    span.textContent = text;
    return span;
  }

  // currentRoute()
  // Purpose: capture the current browser-restorable workspace location.
  // Input: none.
  // Output: route hash with collection, request, and tab ids.
  function currentRoute() {
    var tab = activeTab();
    return {
      collection: state.selected_collection_id || (tab && tab.collection_id) || '',
      request: state.selected_request_id || (tab && tab.request_id) || '',
      tab: state.active_tab_id || ''
    };
  }

  // routeQuery(route)
  // Purpose: serialize a workspace location into URL query parameters.
  // Input: route hash.
  // Output: query string without leading question mark.
  function routeQuery(route) {
    var params = new window.URLSearchParams();
    if (route.collection) params.set('collection', route.collection);
    if (route.request) params.set('request', route.request);
    if (route.tab) params.set('tab', route.tab);
    return params.toString();
  }

  // readRoute()
  // Purpose: parse the current browser URL into a workspace location.
  // Input: none.
  // Output: route hash.
  function readRoute() {
    var params = new window.URLSearchParams(window.location.search || '');
    return {
      collection: params.get('collection') || '',
      request: params.get('request') || '',
      tab: params.get('tab') || ''
    };
  }

  // syncRoute(replaceRoute)
  // Purpose: keep browser history aligned with the active workspace selection.
  // Input: boolean replace flag.
  // Output: none.
  function syncRoute(replaceRoute) {
    if (!window.history || !window.URLSearchParams) return;
    var query = routeQuery(currentRoute());
    var target = window.location.pathname + (query ? '?' + query : '');
    var current = window.location.pathname + window.location.search;
    if (target === current) return;
    if (replaceRoute) {
      window.history.replaceState(currentRoute(), '', target);
      return;
    }
    window.history.pushState(currentRoute(), '', target);
  }

  // applyRoute(route, options)
  // Purpose: restore workspace selection from a direct URL or browser navigation event.
  // Input: route hash and options hash.
  // Output: none.
  function applyRoute(route, options) {
    options = options || {};
    if (route.collection && findCollection(route.collection)) {
      state.selected_collection_id = route.collection;
    }
    if (route.tab) {
      var existingTab = findTab(route.tab);
      if (existingTab) {
        state.active_tab_id = existingTab.id;
        state.selected_collection_id = existingTab.collection_id || state.selected_collection_id;
        state.selected_request_id = existingTab.request_id || route.request || '';
        return;
      }
    }
    if (route.request) {
      var found = findRequestAcrossCollections(route.request);
      if (found) {
        state.selected_collection_id = found.collection.id;
        state.selected_request_id = found.node.id;
        var existingRequestTab = findTabForRequest(found.collection.id, found.node.id);
        if (existingRequestTab) {
          state.active_tab_id = existingRequestTab.id;
        } else if (options.allow_open !== false) {
          var opened = tabFromRequest(found.node, found.collection.id);
          state.tabs.push(opened);
          state.active_tab_id = opened.id;
        }
        return;
      }
    }
    if (!state.selected_request_id && route.collection) {
      state.selected_request_id = '';
    }
  }

  // persistWorkspace(replaceRoute)
  // Purpose: persist local state and synchronize the URL for browser navigation.
  // Input: boolean replace flag.
  // Output: none.
  function persistWorkspace(replaceRoute) {
    saveState();
    syncRoute(!!replaceRoute);
  }

  // renderPreview(payload)
  // Purpose: render browser previews for previewable media responses.
  // Input: normalized response payload.
  // Output: none.
  function renderPreview(payload) {
    var preview = document.getElementById('api-response-preview');
    preview.className = 'api-response-preview';
    preview.innerHTML = '';
    if (!payload || !payload.ok || payload.response.body_mode !== 'preview' || !payload.response.preview_url) {
      return;
    }
    var mediaType = payload.response.preview_media_type || '';
    var node;
    if (/^image\/(?!tiff?)/i.test(mediaType)) {
      node = document.createElement('img');
      node.src = payload.response.preview_url;
      node.alt = 'API response preview';
    } else if (/pdf/i.test(mediaType)) {
      node = document.createElement('iframe');
      node.src = payload.response.preview_url;
      node.title = 'PDF response preview';
    } else {
      node = document.createElement('object');
      node.data = payload.response.preview_url;
      node.type = mediaType || 'application/octet-stream';
    }
    preview.appendChild(node);
    preview.className += ' is-visible';
  }

  // renderResponse(payload)
  // Purpose: render request and response details for the active tab.
  // Input: normalized response payload or null.
  // Output: none.
  function renderResponse(payload) {
    var meta = document.getElementById('api-response-meta');
    var preview = document.getElementById('api-response-preview');
    var body = document.getElementById('api-response-body');
    var headers = document.getElementById('api-response-headers');
    var request = document.getElementById('api-request-details');
    var requestPayload = payload && payload.request || {};
    var responsePayload = payload && payload.response || {};
    meta.innerHTML = '';
    preview.className = 'api-response-preview';
    preview.innerHTML = '';
    if (!payload) {
      body.textContent = 'Ready.';
      headers.textContent = 'Ready.';
      request.textContent = 'Ready.';
      return;
    }
    request.textContent = [
      'Method: ' + (requestPayload.method || ''),
      'URL: ' + (requestPayload.url || ''),
      'Timeout: ' + (requestPayload.timeout_s || 120) + 's',
      'Follow Redirects: ' + (requestPayload.follow_redirects === false ? 'No' : 'Yes'),
      'Allow Insecure TLS: ' + (requestPayload.insecure_tls ? 'Yes' : 'No'),
      '',
      'Headers:',
      requestPayload.headers_text || '(none)',
      '',
      'Body:',
      requestPayload.body || '(empty)'
    ].join('\n');
    if (!payload.ok) {
      meta.appendChild(responseChip('Request failed', true));
      body.textContent = payload.error || 'Request failed.';
      headers.textContent = '';
      return;
    }
    meta.appendChild(responseChip('HTTP ' + responsePayload.status));
    meta.appendChild(responseChip((responsePayload.elapsed_ms || 0) + 'ms'));
    meta.appendChild(responseChip(responsePayload.content_type || 'unknown content type'));
    meta.appendChild(responseChip((responsePayload.body_size_bytes || 0) + ' bytes'));
    if (responsePayload.body_mode === 'preview') {
      body.textContent = 'Preview rendered below.';
      renderPreview(payload);
    } else {
      body.textContent = responsePayload.body || '';
    }
    headers.textContent = (responsePayload.headers || []).map(function (entry) {
      return entry.key + ': ' + entry.value;
    }).join('\n');
  }

  // currentLocationText()
  // Purpose: build a human-readable breadcrumb for the left panel.
  // Input: none.
  // Output: string.
  function currentLocationText() {
    var collection = findCollection(state.selected_collection_id);
    var tab = activeTab();
    var parts = ['Current Location:'];
    if (collection) parts.push(collection.name);
    if (state.selected_request_id) {
      var found = findRequestAcrossCollections(state.selected_request_id);
      if (found) parts.push(found.node.name);
    } else if (tab && tab.title) {
      parts.push(tab.title);
    } else {
      parts.push('Workspace root');
    }
    return parts.join(' / ');
  }

  // renderNodeList(collectionId, items)
  // Purpose: render one nested collection tree branch.
  // Input: collection id and item array.
  // Output: ul element.
  function renderNodeList(collectionId, items) {
    var list = document.createElement('ul');
    list.className = 'api-node-list';
    if (!items.length) {
      var empty = document.createElement('li');
      empty.className = 'api-empty';
      empty.textContent = 'No requests saved in this collection yet.';
      list.appendChild(empty);
      return list;
    }
    items.forEach(function (item) {
      var li = document.createElement('li');
      var button = document.createElement('button');
      button.type = 'button';
      button.className = 'api-node-button' + (item.kind === 'folder' ? ' api-node-folder' : '');
      if (item.kind === 'request' && item.id === state.selected_request_id) button.className += ' is-active';
      button.textContent = item.kind === 'folder' ? 'Folder: ' + item.name : item.name;
      button.addEventListener('click', function () {
        state.selected_collection_id = collectionId;
        if (item.kind !== 'folder') {
          state.selected_request_id = item.id;
          openNodeInTab(collectionId, item.id);
          return;
        }
        persistWorkspace(false);
        renderAll();
      });
      li.appendChild(button);
      if (item.kind === 'folder') li.appendChild(renderNodeList(collectionId, item.item || []));
      list.appendChild(li);
    });
    return list;
  }

  // renderCollections()
  // Purpose: render the collection sidebar and location breadcrumb.
  // Input: none.
  // Output: none.
  function renderCollections() {
    var tree = document.getElementById('api-collection-tree');
    tree.innerHTML = '';
    document.getElementById('api-collection-location').textContent = currentLocationText();
    var activeCollection = findCollection(state.selected_collection_id) || state.collections[0];
    if (!activeCollection) return;

    var tabList = document.createElement('div');
    tabList.className = 'api-collection-tabs';
    tabList.setAttribute('role', 'tablist');
    tabList.setAttribute('aria-label', 'Stored Collections');

    state.collections.forEach(function (collection) {
      var button = document.createElement('button');
      button.type = 'button';
      button.className = 'api-collection-tab';
      button.textContent = collection.name;
      button.setAttribute('role', 'tab');
      button.setAttribute('aria-selected', collection.id === activeCollection.id ? 'true' : 'false');
      button.setAttribute('aria-controls', 'api-collection-panel');
      button.addEventListener('click', function () {
        state.selected_collection_id = collection.id;
        state.selected_request_id = '';
        persistWorkspace(false);
        renderAll();
      });
      tabList.appendChild(button);
    });
    tree.appendChild(tabList);

    var card = document.createElement('section');
    card.className = 'api-collection-card is-active';
    card.id = 'api-collection-panel';
    card.setAttribute('role', 'tabpanel');
    card.setAttribute('data-api-collection-panel', activeCollection.name);
    var title = document.createElement('h3');
    title.textContent = activeCollection.name;
    card.appendChild(title);
    if (activeCollection.description) {
      var note = document.createElement('p');
      note.textContent = activeCollection.description;
      note.style.color = '#6a767b';
      note.style.margin = '0 0 10px';
      card.appendChild(note);
    }
    card.appendChild(renderNodeList(activeCollection.id, activeCollection.item || []));
    tree.appendChild(card);
  }

  // renderShellTabs()
  // Purpose: toggle the top-level Collections and Workspace tab panels.
  // Input: none.
  // Output: none.
  function renderShellTabs() {
    ['collections', 'workspace'].forEach(function (name) {
      var button = document.getElementById('api-shell-tab-' + name);
      var panel = document.getElementById('api-shell-panel-' + name);
      var selected = state.active_shell_tab === name;
      button.setAttribute('aria-selected', selected ? 'true' : 'false');
      panel.hidden = !selected;
    });
  }

  // renderTabs()
  // Purpose: render the open request tabs and keep them browser-addressable.
  // Input: none.
  // Output: none.
  function renderTabs() {
    var strip = document.getElementById('api-tab-strip');
    strip.innerHTML = '';
    state.tabs.forEach(function (tab) {
      var chip = document.createElement('div');
      chip.className = 'api-tab' + (tab.id === state.active_tab_id ? ' is-active' : '');
      chip.addEventListener('click', function () {
        state.active_tab_id = tab.id;
        state.selected_collection_id = tab.collection_id || state.selected_collection_id;
        state.selected_request_id = tab.request_id || '';
        persistWorkspace(false);
        renderAll();
      });
      var label = document.createElement('span');
      label.textContent = tab.title || 'Untitled Tab';
      chip.appendChild(label);
      var close = document.createElement('button');
      close.type = 'button';
      close.className = 'api-tab-close';
      close.textContent = 'x';
      close.addEventListener('click', function (event) {
        event.stopPropagation();
        closeTab(tab.id);
      });
      chip.appendChild(close);
      strip.appendChild(chip);
    });
  }

  // renderEditor()
  // Purpose: populate the editor form and response area from the active tab.
  // Input: none.
  // Output: none.
  function renderEditor() {
    ensureActiveTab();
    var tab = activeTab();
    if (!tab) {
      document.getElementById('api-token-shell').hidden = true;
      renderResponse(null);
      return;
    }
    var collection = findCollection(tab.collection_id || state.selected_collection_id) || findCollection(state.selected_collection_id);
    document.getElementById('api-request-name').value = tab.request.name || tab.title || '';
    document.getElementById('api-request-method').value = tab.request.method || 'GET';
    renderResolvedRequestFields(collection, tab);
    document.getElementById('api-request-description').value = tab.request.description || '';
    document.getElementById('api-timeout').value = tab.request.timeout_s || 120;
    document.getElementById('api-follow-redirects').checked = tab.request.follow_redirects !== false;
    document.getElementById('api-insecure-tls').checked = !!tab.request.insecure_tls;
    document.getElementById('api-request-variables').value = formatVariables(collection ? collection.variable : []);
    document.getElementById('api-collection-name').value = collection ? collection.name : '';
    renderTokenFields(collection, tab);
    renderResponse(tab.response || null);
  }

  // renderResponseTabs()
  // Purpose: toggle the inner request/response tab panels.
  // Input: none.
  // Output: none.
  function renderResponseTabs() {
    ['request', 'body', 'headers'].forEach(function (name) {
      var button = document.getElementById('api-response-tab-' + name);
      var panel = document.getElementById('api-response-panel-' + name);
      var selected = state.active_response_tab === name;
      button.setAttribute('aria-selected', selected ? 'true' : 'false');
      panel.hidden = !selected;
    });
  }

  // syncActiveTabFromForm()
  // Purpose: copy the current form values back into the active tab state.
  // Input: none.
  // Output: none.
  function syncActiveTabFromForm() {
    var tab = activeTab();
    if (!tab) return;
    var collection = findCollection(state.selected_collection_id) || findCollection(tab.collection_id) || null;
    tab.request.name = document.getElementById('api-request-name').value.trim() || 'New Request';
    tab.request.method = document.getElementById('api-request-method').value;
    tab.request.url = readResolvedFieldValue('api-request-url', collection).trim();
    tab.request.headers_text = readResolvedFieldValue('api-request-headers', collection);
    tab.request.body = readResolvedFieldValue('api-request-body', collection);
    tab.request.description = document.getElementById('api-request-description').value;
    tab.request.timeout_s = parseInt(document.getElementById('api-timeout').value || '120', 10) || 120;
    tab.request.follow_redirects = document.getElementById('api-follow-redirects').checked;
    tab.request.insecure_tls = document.getElementById('api-insecure-tls').checked;
    tab.title = tab.request.name;
    if (collection) {
      collection.variable = parseVariablesText(document.getElementById('api-request-variables').value);
      ensureRequestTokens(collection, tab.request);
      if (!tab.collection_id) tab.collection_id = collection.id;
    }
    saveState();
    renderTabs();
  }

  // closeTab(tabId)
  // Purpose: close one request tab and keep selection valid.
  // Input: tab id string.
  // Output: none.
  function closeTab(tabId) {
    state.tabs = state.tabs.filter(function (tab) { return tab.id !== tabId; });
    if (!state.tabs.length) state.tabs.push(tabFromRequest(findFirstRequest(state.collections[0]), state.collections[0].id));
    ensureActiveTab();
    var tab = activeTab();
    state.selected_collection_id = (tab && tab.collection_id) || state.selected_collection_id;
    state.selected_request_id = (tab && tab.request_id) || '';
    persistWorkspace(true);
    renderAll();
  }

  // openNodeInTab(collectionId, nodeId)
  // Purpose: open or focus a saved request from the collection tree.
  // Input: collection id and request id strings.
  // Output: none.
  function openNodeInTab(collectionId, nodeId) {
    var collection = findCollection(collectionId);
    if (!collection) return;
    var node = findNode(collection.item || [], nodeId);
    if (!node || node.kind !== 'request') return;
    setShellTab('workspace');
    state.selected_collection_id = collectionId;
    state.selected_request_id = node.id;
    var existing = findTabForRequest(collectionId, nodeId);
    if (existing) {
      state.active_tab_id = existing.id;
    } else {
      state.tabs.push(tabFromRequest(node, collectionId));
      state.active_tab_id = state.tabs[state.tabs.length - 1].id;
    }
    persistWorkspace(false);
    renderAll();
  }

  // createBlankTab()
  // Purpose: open a new unsaved request tab.
  // Input: none.
  // Output: none.
  function createBlankTab() {
    var collection = findCollection(state.selected_collection_id) || state.collections[0];
    setShellTab('workspace');
    state.tabs.push({
      id: uid('tab'),
      title: 'New Tab',
      collection_id: collection ? collection.id : '',
      request_id: '',
      request: defaultRequest('New Request'),
      response: null
    });
    state.active_tab_id = state.tabs[state.tabs.length - 1].id;
    state.selected_request_id = '';
    persistWorkspace(false);
    renderAll();
  }

  // duplicateActiveTab()
  // Purpose: clone the active request into a new tab.
  // Input: none.
  // Output: none.
  function duplicateActiveTab() {
    var tab = activeTab();
    if (!tab) return;
    setShellTab('workspace');
    var copy = clone(tab);
    copy.id = uid('tab');
    copy.title = (copy.title || 'Request') + ' Copy';
    copy.request.name = copy.title;
    copy.request_id = '';
    state.tabs.push(copy);
    state.active_tab_id = copy.id;
    state.selected_request_id = '';
    persistWorkspace(false);
    renderAll();
  }

  // saveRequestToCollection()
  // Purpose: save or update the active request inside the selected collection.
  // Input: none.
  // Output: none.
  function saveRequestToCollection() {
    syncActiveTabFromForm();
    setShellTab('workspace');
    var tab = activeTab();
    var collection = findCollection(state.selected_collection_id) || findCollection(tab.collection_id) || state.collections[0];
    if (!collection) return;
    state.selected_collection_id = collection.id;
    tab.collection_id = collection.id;
    if (tab.request_id) {
      var existing = findNode(collection.item || [], tab.request_id);
      if (existing && existing.kind === 'request') {
        existing.name = tab.request.name;
        existing.request = clone(tab.request);
        state.selected_request_id = existing.id;
        persistWorkspace(true);
        renderAll();
        persistCollectionToServer(collection, {
          successMessage: 'Updated request "' + existing.name + '" in ' + collection.name + '.',
          failureMessage: 'Unable to save the updated request to ' + collection.name + '.'
        });
        return;
      }
    }
    var node = {
      id: uid('request'),
      kind: 'request',
      name: tab.request.name,
      request: clone(tab.request)
    };
    collection.item.push(node);
    tab.request_id = node.id;
    state.selected_request_id = node.id;
    persistWorkspace(true);
    renderAll();
    persistCollectionToServer(collection, {
      successMessage: 'Saved request "' + node.name + '" to ' + collection.name + '.',
      failureMessage: 'Unable to save the request to ' + collection.name + '.'
    });
  }

  // createCollection()
  // Purpose: add a new empty collection to the workspace.
  // Input: none.
  // Output: none.
  function createCollection() {
    var name = window.prompt('Collection name', 'New Collection');
    if (!name) return;
    name = String(name || '').trim();
    if (!name) return;
    if (collectionNameExists(name)) {
      showBanner('A collection named "' + name + '" already exists.', true);
      return;
    }
    var collection = {
      id: uid('collection'),
      name: name,
      description: '',
      variable: [],
      item: []
    };
    setShellTab('collections');
    state.collections.push(collection);
    state.selected_collection_id = state.collections[state.collections.length - 1].id;
    state.selected_request_id = '';
    persistWorkspace(false);
    renderAll();
    persistCollectionToServer(collection, {
      successMessage: 'Created collection "' + name + '".',
      failureMessage: 'Unable to create collection "' + name + '".'
    });
  }

  // renameSelectedCollection()
  // Purpose: rename the active collection.
  // Input: none.
  // Output: none.
  function renameSelectedCollection() {
    var collection = findCollection(state.selected_collection_id) || state.collections[0];
    if (!collection) return;
    var name = window.prompt('Rename collection', collection.name);
    if (!name) return;
    name = String(name || '').trim();
    if (!name) return;
    if (collectionNameExists(name, collection.id)) {
      showBanner('A collection named "' + name + '" already exists.', true);
      return;
    }
    var originalName = collection.name;
    setShellTab('collections');
    collection.name = name;
    persistWorkspace(true);
    renderAll();
    persistCollectionToServer(collection, {
      original_name: originalName,
      successMessage: 'Renamed collection to "' + name + '".',
      failureMessage: 'Unable to rename collection "' + originalName + '".'
    });
  }

  // deleteSelectedCollection()
  // Purpose: remove the active collection and any tabs that depend on it.
  // Input: none.
  // Output: none.
  function deleteSelectedCollection() {
    var collection = findCollection(state.selected_collection_id) || state.collections[0];
    if (!collection) return;
    if (!window.confirm('Delete collection "' + collection.name + '"?')) return;
    var deletedName = collection.name;
    setShellTab('collections');
    state.collections = state.collections.filter(function (item) {
      return item.id !== collection.id;
    });
    state.tabs = state.tabs.filter(function (tab) {
      return tab.collection_id !== collection.id;
    });
    if (!state.collections.length) state.collections = [scratchCollection()];
    if (!state.tabs.length) state.tabs = [tabFromRequest(findFirstRequest(state.collections[0]), state.collections[0].id)];
    state.selected_collection_id = state.collections[0].id;
    state.selected_request_id = '';
    ensureActiveTab();
    persistWorkspace(false);
    renderAll();
    deleteCollectionFromServer(deletedName, {
      successMessage: 'Deleted collection "' + deletedName + '".',
      failureMessage: 'Unable to delete collection "' + deletedName + '".'
    });
  }

  // removeNode(items, nodeId)
  // Purpose: delete one request node from a nested collection tree.
  // Input: item array and node id string.
  // Output: boolean success flag.
  function removeNode(items, nodeId) {
    var i;
    for (i = 0; i < (items || []).length; i++) {
      var item = items[i];
      if (item.id === nodeId) {
        items.splice(i, 1);
        return true;
      }
      if (item.kind === 'folder' && removeNode(item.item || [], nodeId)) return true;
    }
    return false;
  }

  // deleteSavedRequest()
  // Purpose: remove the active saved request from its collection tree.
  // Input: none.
  // Output: none.
  function deleteSavedRequest() {
    var tab = activeTab();
    if (!tab || !tab.request_id) {
      showBanner('The active tab is not linked to a saved request.', true);
      return;
    }
    var collection = findCollection(tab.collection_id || state.selected_collection_id);
    if (!collection) return;
    if (!window.confirm('Delete saved request "' + (tab.request.name || tab.title) + '"?')) return;
    if (!removeNode(collection.item || [], tab.request_id)) {
      showBanner('Unable to delete the selected request from the collection tree.', true);
      return;
    }
    tab.request_id = '';
    state.selected_request_id = '';
    persistWorkspace(true);
    renderAll();
    persistCollectionToServer(collection, {
      successMessage: 'Deleted the saved request from ' + collection.name + '.',
      failureMessage: 'Unable to update ' + collection.name + ' after deleting the request.'
    });
  }

  // importRequest(request)
  // Purpose: convert a Postman request object into the local request model.
  // Input: Postman request hash.
  // Output: local request hash.
  function importRequest(request) {
    var url = '';
    if (request && request.url) {
      url = typeof request.url === 'string' ? request.url : (request.url.raw || '');
    }
    return {
      method: request && request.method ? String(request.method).toUpperCase() : 'GET',
      url: url,
      headers_text: (request && Array.isArray(request.header) ? request.header : []).map(function (header) {
        return (header.key || '') + ': ' + (header.value || '');
      }).join('\n'),
      body: request && request.body && typeof request.body.raw === 'string' ? request.body.raw : '',
      description: request && request.description ? String(request.description) : '',
      timeout_s: 120,
      insecure_tls: false,
      follow_redirects: true,
      name: request && request.name ? request.name : 'Request'
    };
  }

  // importPostmanNode(node)
  // Purpose: convert one imported Postman tree node into local state.
  // Input: Postman node hash.
  // Output: local node hash or null.
  function importPostmanNode(node) {
    if (!node || typeof node !== 'object') return null;
    if (Array.isArray(node.item)) {
      return {
        id: uid('folder'),
        kind: 'folder',
        name: node.name || 'Folder',
        item: node.item.map(importPostmanNode).filter(Boolean)
      };
    }
    var request = importRequest(node.request || node);
    return {
      id: uid('request'),
      kind: 'request',
      name: node.name || request.name || 'Request',
      request: request
    };
  }

  // importPostmanCollection(payload)
  // Purpose: validate and normalize an imported Postman collection payload.
  // Input: parsed JSON payload.
  // Output: local collection hash.
  function importPostmanCollection(payload) {
    var source = payload && payload.collection ? payload.collection : payload;
    if (!source || typeof source !== 'object') throw new Error('The imported JSON must contain a Postman collection object.');
    if (!source.info || !source.info.name) throw new Error('The imported collection is missing info.name.');
    return normalizeCollection({
      name: source.info.name,
      description: source.info.description || '',
      variable: source.variable || [],
      item: (source.item || []).map(importPostmanNode).filter(Boolean)
    });
  }

  // exportPostmanNode(node)
  // Purpose: convert one local node back into Postman-compatible JSON.
  // Input: local node hash.
  // Output: Postman node hash.
  function exportPostmanNode(node) {
    if (node.kind === 'folder') {
      return {
        name: node.name,
        item: (node.item || []).map(exportPostmanNode)
      };
    }
    return {
      name: node.name,
      request: {
        method: node.request.method || 'GET',
        header: String(node.request.headers_text || '').split(/\n/).map(function (line) {
          var trimmed = line.trim();
          if (!trimmed) return null;
          var pos = trimmed.indexOf(':');
          if (pos < 1) return null;
          return {
            key: trimmed.slice(0, pos).trim(),
            value: trimmed.slice(pos + 1).trim()
          };
        }).filter(Boolean),
        body: node.request.body ? { mode: 'raw', raw: node.request.body } : undefined,
        url: { raw: node.request.url || '' },
        description: node.request.description || ''
      }
    };
  }

  // exportPostmanCollection(collection)
  // Purpose: convert one local collection back into Postman v2.1 JSON.
  // Input: local collection hash.
  // Output: Postman collection hash.
  function exportPostmanCollection(collection) {
    return {
      info: {
        name: collection.name,
        description: collection.description || '',
        schema: POSTMAN_SCHEMA
      },
      variable: clone(collection.variable || []),
      item: (collection.item || []).map(exportPostmanNode)
    };
  }

  // importCollectionFile(file, inputElement)
  // Purpose: read and import a Postman collection file from disk.
  // Input: browser File object and optional input element for lifecycle cleanup.
  // Output: none.
  function importCollectionFile(file, inputElement) {
    function finishImportPicker() {
      if (!inputElement) return;
      inputElement.value = '';
      inputElement.dataset.importBusy = '';
    }

    function handleImportedText(text) {
      try {
        var parsed = JSON.parse(text);
        var collection = importPostmanCollection(parsed);
        if (collectionNameExists(collection.name)) {
          showBanner('A collection named "' + collection.name + '" already exists.', true);
          return;
        }
        setShellTab('collections');
        $.ajax({
          method: 'POST',
          url: configs.collections.save,
          dataType: 'json',
          data: {
            collection: JSON.stringify(exportPostmanCollection(collection)),
            original_name: ''
          }
        }).done(function (response) {
          try {
            requireAjaxSuccess(response, 'Import failed: the collection save route returned an empty success response.');
          } catch (error) {
            showBanner('Import failed: ' + error.message, true);
            return;
          }
          reloadCollectionsFromServer({
            failureMessage: 'Imported collection "' + collection.name + '" but failed to reload collections.',
            afterSuccess: function () {
              var imported = findCollectionByName(collection.name);
              if (imported) {
                state.selected_collection_id = imported.id;
                state.selected_request_id = '';
                persistWorkspace(true);
              }
              showBanner('Imported Postman collection "' + collection.name + '".', false);
            }
          });
        }).fail(function (xhr) {
          showBanner('Import failed: ' + (xhr.responseText || 'Unable to store imported collection.'), true);
        });
      } catch (error) {
        showBanner('Import failed: ' + error.message, true);
      } finally {
        finishImportPicker();
      }
    }

    function readCollectionFileText(fileToRead) {
      return new Promise(function (resolve, reject) {
        if (!fileToRead) {
          reject(new Error('No collection file selected.'));
          return;
        }
        if (typeof FileReader === 'function') {
          try {
            var reader = new FileReader();
            reader.onload = function () {
              resolve(reader.result);
            };
            reader.onerror = function () {
              reject(new Error('Unable to read the selected collection file.'));
            };
            reader.readAsText(fileToRead);
            return;
          } catch (error) {
            if (typeof fileToRead.text !== 'function') {
              reject(error);
              return;
            }
          }
        }
        if (typeof fileToRead.text === 'function') {
          fileToRead.text().then(resolve).catch(function () {
            reject(new Error('Unable to read the selected collection file.'));
          });
          return;
        }
        reject(new Error('Unable to read the selected collection file.'));
      });
    }

    if (!file) {
      finishImportPicker();
      return;
    }
    readCollectionFileText(file).then(handleImportedText).catch(function (error) {
      showBanner('Import failed: ' + (error && error.message ? error.message : 'Unable to read the selected collection file.'), true);
      finishImportPicker();
    });
  }

  // exportCurrentCollection()
  // Purpose: download the selected collection in Postman-compatible JSON.
  // Input: none.
  // Output: none.
  function exportCurrentCollection() {
    var collection = findCollection(state.selected_collection_id) || state.collections[0];
    if (!collection) {
      showBanner('Choose a collection to export first.', true);
      return;
    }
    setShellTab('collections');
    var payload = exportPostmanCollection(collection);
    var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    var url = window.URL.createObjectURL(blob);
    var link = document.createElement('a');
    link.href = url;
    link.download = collection.name.replace(/[^A-Za-z0-9._-]+/g, '-') + '.postman_collection.json';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(url);
  }

  // bootstrapCollectionsFromPayload(payload)
  // Purpose: normalize collection bootstrap ajax payloads.
  // Input: bootstrap payload.
  // Output: normalized collection array.
  function bootstrapCollectionsFromPayload(payload) {
    var raw = [];
    if (Array.isArray(payload)) raw = payload;
    else if (payload && Array.isArray(payload.collections)) raw = payload.collections;
    else if (payload && typeof payload === 'object') raw = Object.keys(payload).map(function (key) { return payload[key]; });
    return raw.map(importPostmanCollection).filter(Boolean);
  }

  // requireAjaxSuccess(response, fallbackMessage)
  // Purpose: reject saved Ajax responses that returned HTTP 200 without the expected ok payload.
  // Input: decoded Ajax response value and a fallback error message string.
  // Output: the original response when it reports ok, otherwise throws an Error.
  function requireAjaxSuccess(response, fallbackMessage) {
    if (response && response.ok) return response;
    if (response && response.error) throw new Error(response.error);
    throw new Error(fallbackMessage || 'The request did not report success.');
  }

  // applyBootstrapCollections(bootstrapCollections)
  // Purpose: replace the current collection set with the file-backed bootstrap collections while preserving local ids when possible.
  // Input: normalized collection array.
  // Output: none.
  function applyBootstrapCollections(bootstrapCollections) {
    var previousByName = {};
    (state.collections || []).forEach(function (collection) {
      previousByName[collection.name] = collection;
    });
    var nextCollections = (bootstrapCollections || []).map(function (collection) {
      if (previousByName[collection.name]) adoptCollectionIds(previousByName[collection.name], collection);
      return collection;
    });
    if (!nextCollections.length) nextCollections = [scratchCollection()];
    state.collections = nextCollections;
    state.tabs = (state.tabs || []).filter(function (tab) {
      return !!findCollection(tab.collection_id);
    });
    if (!state.tabs.length) {
      state.tabs = [tabFromRequest(findFirstRequest(state.collections[0]), state.collections[0].id)];
    }
    ensureActiveTab();
    if (!findCollection(state.selected_collection_id) && state.collections.length) {
      state.selected_collection_id = state.collections[0].id;
    }
    if (state.selected_request_id && !findRequestAcrossCollections(state.selected_request_id)) {
      state.selected_request_id = '';
    }
    var tab = activeTab();
    if (tab) {
      state.selected_collection_id = tab.collection_id || state.selected_collection_id;
      if (tab.request_id && !findRequestAcrossCollections(tab.request_id)) tab.request_id = '';
      if (!state.selected_request_id) state.selected_request_id = tab.request_id || '';
    }
  }

  // reloadCollectionsFromServer(options)
  // Purpose: reload all file-backed collections from the runtime ajax bootstrap endpoint and refresh the current workspace state.
  // Input: optional options hash with failureMessage.
  // Output: jqXHR-style ajax handle.
  function reloadCollectionsFromServer(options) {
    options = options || {};
    return $.ajax({
      method: 'GET',
      url: configs.collections.bootstrap,
      dataType: 'json'
    }).done(function (payload) {
      try {
        if (!payload || typeof payload !== 'object' || !Array.isArray(payload.collections)) {
          throw new Error('The collection bootstrap response was not valid JSON.');
        }
        applyBootstrapCollections(bootstrapCollectionsFromPayload(payload));
        applyRoute(readRoute(), { allow_open: true });
        if (typeof options.afterSuccess === 'function') options.afterSuccess(payload);
        persistWorkspace(true);
      } catch (error) {
        showBanner('Collection bootstrap failed: ' + error.message, true);
      }
      if (payload && payload.errors && payload.errors.length) showBanner(payload.errors.join(' | '), true);
      renderAll();
    }).fail(function (xhr) {
      showBanner(options.failureMessage || ('Collection bootstrap failed: ' + (xhr.responseText || 'unknown error')), true);
      renderAll();
    });
  }

  // persistCollectionToServer(collection, options)
  // Purpose: write one collection to config/api-dashboard as Postman JSON.
  // Input: local collection hash and optional options hash with original_name, successMessage, and failureMessage.
  // Output: jqXHR-style ajax handle.
  function persistCollectionToServer(collection, options) {
    options = options || {};
    return $.ajax({
      method: 'POST',
      url: configs.collections.save,
      dataType: 'json',
      data: {
        collection: JSON.stringify(exportPostmanCollection(collection)),
        original_name: options.original_name || ''
      }
    }).done(function (response) {
      try {
        requireAjaxSuccess(response, 'Collection save failed: the save route returned an empty success response.');
        persistWorkspace(true);
        renderAll();
        showBanner(options.successMessage || ('Saved collection "' + collection.name + '".'), false);
      } catch (error) {
        reloadCollectionsFromServer({
          failureMessage: options.failureMessage || ('Collection save failed: ' + error.message)
        });
      }
    }).fail(function (xhr) {
      reloadCollectionsFromServer({
        failureMessage: options.failureMessage || ('Collection save failed: ' + (xhr.responseText || 'unknown error'))
      });
    });
  }

  // deleteCollectionFromServer(name, options)
  // Purpose: delete one stored collection file from config/api-dashboard.
  // Input: collection name string and optional options hash with successMessage and failureMessage.
  // Output: jqXHR-style ajax handle.
  function deleteCollectionFromServer(name, options) {
    options = options || {};
    return $.ajax({
      method: 'POST',
      url: configs.collections.delete,
      dataType: 'json',
      data: {
        name: name
      }
    }).done(function (response) {
      try {
        requireAjaxSuccess(response, 'Collection delete failed: the delete route returned an empty success response.');
        persistWorkspace(true);
        renderAll();
        showBanner(options.successMessage || ('Deleted collection "' + name + '".'), false);
      } catch (error) {
        reloadCollectionsFromServer({
          failureMessage: options.failureMessage || ('Collection delete failed: ' + error.message)
        });
      }
    }).fail(function (xhr) {
      reloadCollectionsFromServer({
        failureMessage: options.failureMessage || ('Collection delete failed: ' + (xhr.responseText || 'unknown error'))
      });
    });
  }

  // sendActiveRequest()
  // Purpose: dispatch the active request through the saved Ajax LWP sender.
  // Input: none.
  // Output: none.
  function sendActiveRequest() {
    syncActiveTabFromForm();
    setShellTab('workspace');
    setResponseTab('body');
    var tab = activeTab();
    var collection = findCollection(state.selected_collection_id) || findCollection(tab.collection_id) || state.collections[0];
    if (!tab || !tab.request.url) {
      renderResponse({ ok: false, error: 'Enter a request URL first.' });
      return;
    }
    renderResponse({ ok: false, error: 'Sending request...' });
    var payload = clone(tab.request);
    payload.url = applyVariables(collection, payload.url);
    payload.headers_text = applyVariables(collection, payload.headers_text);
    payload.body = applyVariables(collection, payload.body);
    $.ajax({
      method: 'POST',
      url: configs.send.request,
      dataType: 'json',
      data: {
        settings: JSON.stringify(payload)
      }
    }).done(function (response) {
      tab.response = normalizeResponse(response);
      saveState();
      renderResponse(tab.response);
    }).fail(function (xhr) {
      tab.response = normalizeResponse({ ok: false, error: xhr.responseText || 'Request dispatch failed.' });
      saveState();
      renderResponse(tab.response);
    });
  }

  // bindForm()
  // Purpose: attach browser event handlers to the workspace controls.
  // Input: none.
  // Output: none.
  function bindForm() {
    ['api-request-name', 'api-request-method', 'api-request-url', 'api-request-headers', 'api-request-body', 'api-request-description', 'api-timeout', 'api-follow-redirects', 'api-insecure-tls', 'api-request-variables'].forEach(function (id) {
      var element = document.getElementById(id);
      var eventName = element && element.tagName === 'SELECT' ? 'change' : 'input';
      if (!element) return;
      element.addEventListener(eventName, syncActiveTabFromForm);
      if (eventName !== 'change') element.addEventListener('change', syncActiveTabFromForm);
    });
    document.getElementById('api-new-collection').addEventListener('click', createCollection);
    document.getElementById('api-rename-collection').addEventListener('click', renameSelectedCollection);
    document.getElementById('api-delete-collection').addEventListener('click', deleteSelectedCollection);
    document.getElementById('api-new-tab').addEventListener('click', createBlankTab);
    document.getElementById('api-duplicate-tab').addEventListener('click', duplicateActiveTab);
    document.getElementById('api-save-request').addEventListener('click', saveRequestToCollection);
    document.getElementById('api-delete-request').addEventListener('click', deleteSavedRequest);
    document.getElementById('api-export-collection').addEventListener('click', exportCurrentCollection);
    document.getElementById('api-send-request').addEventListener('click', sendActiveRequest);
    document.getElementById('api-clear-response').addEventListener('click', function () {
      setResponseTab('body');
      var tab = activeTab();
      if (tab) tab.response = null;
      saveState();
      renderResponse(null);
      showBanner('', false);
    });
    function importFromPicker(event) {
      if (event.target.dataset.importBusy === '1') return;
      var file = event.target.files && event.target.files[0];
      if (!file) return;
      event.target.dataset.importBusy = '1';
      importCollectionFile(file, event.target);
    }
    document.getElementById('api-import-file').addEventListener('change', importFromPicker);
    ['collections', 'workspace'].forEach(function (name) {
      document.getElementById('api-shell-tab-' + name).addEventListener('click', function () {
        setShellTab(name);
        renderShellTabs();
      });
    });
    ['request', 'body', 'headers'].forEach(function (name) {
      document.getElementById('api-response-tab-' + name).addEventListener('click', function () {
        setResponseTab(name);
        renderResponseTabs();
      });
    });
    window.addEventListener('popstate', function () {
      applyRoute(readRoute(), { allow_open: true });
      persistWorkspace(true);
      renderAll();
    });
  }

  // renderAll()
  // Purpose: render the full workspace UI from the current state.
  // Input: none.
  // Output: none.
  function renderAll() {
    renderShellTabs();
    renderCollections();
    renderTabs();
    renderEditor();
    renderResponseTabs();
  }

  $(document).ready(function () {
    state = loadState();
    bindForm();
    applyRoute(readRoute(), { allow_open: true });
    persistWorkspace(true);
    renderAll();
    reloadCollectionsFromServer();
  });
}());
</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.collections.bootstrap', type => 'json', file => 'api-dashboard-bootstrap', code => q{
  BEGIN {
    my $ajax_file = $ENV{DEVELOPER_DASHBOARD_AJAX_FILE} || '';
    if ( $ajax_file ne '' ) {
      require File::Basename;
      require File::Spec;
      require lib;
      my $root = File::Basename::dirname($ajax_file);
      $root = File::Basename::dirname($root) for 1 .. 4;
      my $candidate = File::Spec->catdir( $root, 'lib' );
      lib->import($candidate) if -d $candidate;
    }
  }

  use File::Path qw(make_path);
  use File::Spec;
  use Developer::Dashboard::Folder ();
  use Developer::Dashboard::JSON qw(json_decode json_encode);
  use Developer::Dashboard::PathRegistry ();

  sub _api_dashboard_collection_root {
    my $dir = File::Spec->catdir( Developer::Dashboard::Folder->configs, 'api-dashboard' );
    make_path($dir) if $dir ne '' && !-d $dir;
    Developer::Dashboard::PathRegistry->new->secure_dir_permissions($dir);
    return $dir;
  }

  sub _api_dashboard_collection_name {
    my ($collection) = @_;
    die 'Collection info hash is required' if ref( $collection->{info} ) ne 'HASH';
    my $name = defined $collection->{info}{name} ? $collection->{info}{name} : '';
    $name =~ s/^\s+//;
    $name =~ s/\s+\z//;
    die 'Collection info.name is required' if $name eq '';
    return $name;
  }

  sub _api_dashboard_coerce_collection {
    my ($input) = @_;
    die 'Collection payload must be a hash reference' if ref($input) ne 'HASH';
    my $collection = ref( $input->{collection} ) eq 'HASH' ? $input->{collection} : $input;
    die 'Collection payload must be a hash reference' if ref($collection) ne 'HASH';
    my $name = _api_dashboard_collection_name($collection);
    $collection->{info}{name} = $name;
    $collection->{info}{schema} ||= 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
    $collection->{item} = [] if ref( $collection->{item} ) ne 'ARRAY';
    $collection->{variable} = [] if ref( $collection->{variable} ) ne 'ARRAY';
    return $collection;
  }

  sub _api_dashboard_slurp {
    my ($file) = @_;
    open my $fh, '<:raw', $file or die "Unable to read $file: $!";
    local $/;
    my $raw = <$fh>;
    close $fh or die "Unable to close $file: $!";
    return $raw;
  }

  my $dir = _api_dashboard_collection_root();
  my @collections;
  my @errors;

  my $dh;
  if ( !opendir( $dh, $dir ) ) {
    print json_encode(
      {
        collections => [],
        errors      => ["Unable to read api-dashboard collection directory $dir: $!"],
      }
    );
    return;
  }

  for my $entry ( sort readdir $dh ) {
    next if $entry eq '.' || $entry eq '..';
    next if $entry !~ /\.json\z/i;

    my $path = File::Spec->catfile( $dir, $entry );
    next if !-f $path;

    my $raw = eval { _api_dashboard_slurp($path) };
    if ($@) {
      push @errors, $@;
      next;
    }

    my $parsed = eval { json_decode($raw) };
    if ($@) {
      push @errors, "Unable to parse Postman JSON in $entry";
      next;
    }

    my $collection = eval { _api_dashboard_coerce_collection($parsed) };
    if ($@) {
      push @errors, "Skipping $entry because it does not contain a Postman collection";
      next;
    }

    push @collections, $collection;
  }

  closedir $dh or push @errors, "Unable to close api-dashboard collection directory $dir: $!";

  print json_encode(
    {
      collections => \@collections,
      errors      => \@errors,
    }
  );
};
:--------------------------------------------------------------------------------:
CODE2: Ajax jvar => 'configs.collections.save', type => 'json', file => 'api-dashboard-collections-save', code => q{
  BEGIN {
    my $ajax_file = $ENV{DEVELOPER_DASHBOARD_AJAX_FILE} || '';
    if ( $ajax_file ne '' ) {
      require File::Basename;
      require File::Spec;
      require lib;
      my $root = File::Basename::dirname($ajax_file);
      $root = File::Basename::dirname($root) for 1 .. 4;
      my $candidate = File::Spec->catdir( $root, 'lib' );
      lib->import($candidate) if -d $candidate;
    }
  }

  use File::Basename qw(basename dirname);
  use File::Path qw(make_path);
  use File::Spec;
  use Developer::Dashboard::Folder ();
  use Developer::Dashboard::JSON qw(json_decode json_encode);
  use Developer::Dashboard::PathRegistry ();

  sub _api_dashboard_paths {
    return Developer::Dashboard::PathRegistry->new;
  }

  sub _api_dashboard_collection_root {
    my $dir = File::Spec->catdir( Developer::Dashboard::Folder->configs, 'api-dashboard' );
    make_path($dir) if $dir ne '' && !-d $dir;
    _api_dashboard_paths()->secure_dir_permissions($dir);
    return $dir;
  }

  sub _api_dashboard_collection_name {
    my ($collection) = @_;
    die 'Collection info hash is required' if ref( $collection->{info} ) ne 'HASH';
    my $name = defined $collection->{info}{name} ? $collection->{info}{name} : '';
    $name =~ s/^\s+//;
    $name =~ s/\s+\z//;
    die 'Collection info.name is required' if $name eq '';
    return $name;
  }

  sub _api_dashboard_safe_filename {
    my ($name) = @_;
    $name = '' if !defined $name;
    $name =~ s/[\x00-\x1F]+/ /g;
    $name =~ s{[\\/:*?"<>|]+}{-}g;
    $name =~ s/\s+/ /g;
    $name =~ s/^\s+//;
    $name =~ s/\s+\z//;
    die 'Collection name is required' if $name eq '' || $name eq '.' || $name eq '..';
    return $name;
  }

  sub _api_dashboard_file_for_name {
    my ($name) = @_;
    return File::Spec->catfile( _api_dashboard_collection_root(), _api_dashboard_safe_filename($name) . '.json' );
  }

  sub _api_dashboard_coerce_collection {
    my ($input) = @_;
    die 'Collection payload must be a hash reference' if ref($input) ne 'HASH';
    my $collection = ref( $input->{collection} ) eq 'HASH' ? $input->{collection} : $input;
    die 'Collection payload must be a hash reference' if ref($collection) ne 'HASH';
    my $name = _api_dashboard_collection_name($collection);
    $collection->{info}{name} = $name;
    $collection->{info}{schema} ||= 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
    $collection->{item} = [] if ref( $collection->{item} ) ne 'ARRAY';
    $collection->{variable} = [] if ref( $collection->{variable} ) ne 'ARRAY';
    return $collection;
  }

  sub _api_dashboard_atomic_write_text {
    my ( $file, $text ) = @_;
    my $tmp = "$file.pending";
    my $dir = dirname($file);
    make_path($dir) if !-d $dir;
    _api_dashboard_paths()->secure_dir_permissions($dir);
    open my $fh, '>:raw', $tmp or die "Unable to write $tmp: $!";
    print {$fh} defined $text ? $text : '';
    close $fh or die "Unable to close $tmp: $!";
    _api_dashboard_paths()->secure_file_permissions($tmp);
    unlink $file if -f $file;
    rename $tmp, $file or die "Unable to rename $tmp to $file: $!";
    _api_dashboard_paths()->secure_file_permissions($file);
    return $file;
  }

  my $collection_raw = params->{collection} // '';
  die "collection payload is required" if $collection_raw eq '';
  my $collection = json_decode($collection_raw);
  die "collection payload must be a hash" if ref($collection) ne 'HASH';

  my $stored = _api_dashboard_coerce_collection($collection);
  my $name = _api_dashboard_collection_name($stored);
  my $file = _api_dashboard_file_for_name($name);
  my $original_name = scalar( params->{original_name} // '' );
  my $original_file = $original_name ne '' ? _api_dashboard_file_for_name($original_name) : '';

  die "Collection '$name' already exists"
    if $original_name ne '' && -f $file && $original_file ne $file;

  _api_dashboard_atomic_write_text( $file, json_encode($stored) );

  if ( $original_file ne '' && $original_file ne $file && -f $original_file ) {
    unlink $original_file or die "Unable to remove renamed collection file $original_file: $!";
  }

  print json_encode(
    {
      ok         => 1,
      collection => $stored,
      filename   => basename($file),
      path       => $file,
    }
  );
};
:--------------------------------------------------------------------------------:
CODE3: Ajax jvar => 'configs.collections.delete', type => 'json', file => 'api-dashboard-collections-delete', code => q{
  BEGIN {
    my $ajax_file = $ENV{DEVELOPER_DASHBOARD_AJAX_FILE} || '';
    if ( $ajax_file ne '' ) {
      require File::Basename;
      require File::Spec;
      require lib;
      my $root = File::Basename::dirname($ajax_file);
      $root = File::Basename::dirname($root) for 1 .. 4;
      my $candidate = File::Spec->catdir( $root, 'lib' );
      lib->import($candidate) if -d $candidate;
    }
  }

  use File::Basename qw(basename);
  use File::Path qw(make_path);
  use File::Spec;
  use Developer::Dashboard::Folder ();
  use Developer::Dashboard::JSON qw(json_encode);
  use Developer::Dashboard::PathRegistry ();

  sub _api_dashboard_collection_root {
    my $dir = File::Spec->catdir( Developer::Dashboard::Folder->configs, 'api-dashboard' );
    make_path($dir) if $dir ne '' && !-d $dir;
    Developer::Dashboard::PathRegistry->new->secure_dir_permissions($dir);
    return $dir;
  }

  sub _api_dashboard_safe_filename {
    my ($name) = @_;
    $name = '' if !defined $name;
    $name =~ s/[\x00-\x1F]+/ /g;
    $name =~ s{[\\/:*?"<>|]+}{-}g;
    $name =~ s/\s+/ /g;
    $name =~ s/^\s+//;
    $name =~ s/\s+\z//;
    die 'Collection name is required' if $name eq '' || $name eq '.' || $name eq '..';
    return $name;
  }

  sub _api_dashboard_file_for_name {
    my ($name) = @_;
    return File::Spec->catfile( _api_dashboard_collection_root(), _api_dashboard_safe_filename($name) . '.json' );
  }

  my $name = params->{name} // '';
  die "collection name is required" if $name eq '';

  my $file = _api_dashboard_file_for_name($name);
  die "Collection '$name' does not exist" if !-f $file;

  unlink $file or die "Unable to remove collection file $file: $!";

  print json_encode(
    {
      ok       => 1,
      filename => basename($file),
      path     => $file,
    }
  );
};
:--------------------------------------------------------------------------------:
CODE4: Ajax jvar => 'configs.send.request', type => 'json', file => 'api-dashboard-send-request', code => q{
  use HTTP::Request;
  use LWP::Protocol::https ();
  use LWP::UserAgent;
  use MIME::Base64 qw(encode_base64);
  use Time::HiRes qw(time);
  use URI ();
  use Developer::Dashboard::DataHelper qw(j je);

  sub _api_dashboard_headers {
    my ($text) = @_;
    my @pairs;
    for my $line ( split /\n/, ( defined $text ? $text : '' ) ) {
      $line =~ s/\r//g;
      next if $line !~ /\S/;
      my ( $key, $value ) = split /\s*:\s*/, $line, 2;
      die "Invalid header line: $line" if !defined $key || !defined $value || $key eq '';
      push @pairs, [ $key, $value ];
    }
    return \@pairs;
  }

  sub _api_dashboard_is_textual {
    my ( $content_type, $content ) = @_;
    return 1 if ( $content_type || '' ) =~ m{(?:json|xml|javascript|html|text|x-www-form-urlencoded)}i;
    return 0 if !defined $content;
    return $content !~ /[\x00-\x08\x0B\x0C\x0E-\x1F]/;
  }

  sub _api_dashboard_preview_media_type {
    my ($content_type) = @_;
    my $type = lc( $content_type || '' );
    $type =~ s/\s*;.*\z//;
    return '' if $type eq '';
    return $type if $type eq 'application/pdf';
    return $type if $type =~ /\Aimage\/(?:png|jpe?g|gif|webp|bmp|svg\+xml|tiff?)\z/;
    return '';
  }

  my $payload = { ok => 0 };

  my $ok = eval {
    my $settings_raw = params->{settings} // '';
    die "settings payload is required" if $settings_raw eq '';
    my $settings = je $settings_raw;
    die "settings payload must be a hash" if ref($settings) ne 'HASH';

    my $method = uc( $settings->{method} || 'GET' );
    die "Unsupported HTTP method: $method" if $method !~ /\A(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\z/;

    my $url = $settings->{url} // '';
    die "Request URL is required" if $url eq '';
    my $uri = URI->new($url);
    die "Request URL must use http or https" if ( ( $uri->scheme || '' ) !~ /\Ahttps?\z/i );

    my $timeout_s = int( $settings->{timeout_s} || 120 );
    $timeout_s = 120 if $timeout_s < 1 || $timeout_s > 600;
    my $allow_insecure  = $settings->{insecure_tls} ? 1 : 0;
    my $follow_redirect = exists $settings->{follow_redirects} ? ( $settings->{follow_redirects} ? 1 : 0 ) : 1;

    my $ua = LWP::UserAgent->new(
      agent             => 'Developer Dashboard API Dashboard/1.60',
      env_proxy         => 1,
      timeout           => $timeout_s,
      max_redirect      => $follow_redirect ? 7 : 0,
      protocols_allowed => [qw(http https)],
      ssl_opts          => $allow_insecure
        ? { verify_hostname => 0, SSL_verify_mode => 0x00 }
        : { verify_hostname => 1 },
    );

    my $request = HTTP::Request->new( $method => $uri->as_string );
    my $header_pairs = _api_dashboard_headers( $settings->{headers_text} );
    for my $pair ( @{$header_pairs} ) {
      $request->header( $pair->[0] => $pair->[1] );
    }
    $request->content( $settings->{body} ) if defined $settings->{body} && $settings->{body} ne '';

    my $started  = time;
    my $response = $ua->request($request);
    my $elapsed_ms = int( ( time - $started ) * 1000 );
    my @headers = map { +{ key => $_, value => scalar $response->header($_) } } $response->header_field_names;
    my $content_type = $response->header('Content-Type') || '';
    my $preview_media_type = _api_dashboard_preview_media_type($content_type);
    my $raw_body = $response->content;
    my $body_mode = 'text';
    my $body = '';
    my $preview_url = '';
    if ($preview_media_type) {
      $body_mode = 'preview';
      $body = 'Preview available.';
      $preview_url = 'data:' . $preview_media_type . ';base64,' . encode_base64( $raw_body, '' );
    }
    elsif ( ( $content_type || '' ) =~ /json/i ) {
      my $decoded = $response->decoded_content;
      my $pretty = eval { j je $decoded };
      $body_mode = 'json';
      $body = defined $pretty && $pretty ne '' ? $pretty : $decoded;
    }
    elsif ( _api_dashboard_is_textual( $content_type, $raw_body ) ) {
      $body_mode = 'text';
      $body = $response->decoded_content;
    }
    else {
      $body_mode = 'text';
      $body = 'Binary response omitted because it is not a previewable media type.';
    }

    $payload = {
      ok      => $response->is_success ? 1 : 0,
      error   => $response->is_success ? '' : $response->status_line,
      request => {
        method           => $method,
        url              => $uri->as_string,
        headers_text     => $settings->{headers_text} // '',
        body             => $settings->{body} // '',
        timeout_s        => $timeout_s,
        follow_redirects => $follow_redirect ? 1 : 0,
        insecure_tls     => $allow_insecure ? 1 : 0,
      },
      response => {
        status             => $response->code,
        reason             => $response->message,
        elapsed_ms         => $elapsed_ms,
        content_type       => $content_type,
        headers            => \@headers,
        body               => $body,
        body_mode          => $body_mode,
        preview_media_type => $preview_media_type,
        preview_url        => $preview_url,
        body_size_bytes    => length($raw_body),
      },
    };
    1;
  };
  if ( !$ok ) {
    my $error = $@ || 'Unknown API dashboard send failure';
    $error =~ s/\s+\z//;
    $payload = {
      ok    => 0,
      error => $error,
    };
  }

  print j $payload;
};
BOOKMARK
}

# _db_dashboard_page()
# Builds the seeded DB dashboard bookmark page as a neutral SQL workspace without embedding connection details or credentials.
# Input: none.
# Output: Developer::Dashboard::PageDocument object.
sub _db_dashboard_page {
    return Developer::Dashboard::PageDocument->new(
        id          => 'db-dashboard',
        title       => 'DB Dashboard',
        description => 'A generic SQL scratchpad and result workspace with no bundled credentials or vendor-specific defaults.',
        layout      => {
            body => <<'HTML',
<style>
.db-dashboard-grid { display:grid; gap:16px; }
.db-dashboard-card { border:1px solid #d9d3c7; padding:16px; background:#fffaf1; }
.db-dashboard-row { display:grid; gap:12px; margin-bottom:12px; }
.db-dashboard-row input, .db-dashboard-row textarea { width:100%; box-sizing:border-box; padding:10px; font-family: monospace; }
.db-dashboard-toolbar { display:flex; gap:12px; flex-wrap:wrap; }
.db-dashboard-result { min-height:220px; white-space:pre-wrap; overflow:auto; background:#1f2a2e; color:#f4efe3; padding:12px; }
</style>
<div class="db-dashboard-grid">
  <section class="db-dashboard-card">
    <h2>Connection Notes</h2>
    <div class="db-dashboard-row">
      <label for="db-target">Target</label>
      <input id="db-target" placeholder="Write the target database or environment name">
    </div>
    <div class="db-dashboard-row">
      <label for="db-query">SQL</label>
      <textarea id="db-query" rows="12" placeholder="select now();"></textarea>
    </div>
    <div class="db-dashboard-toolbar">
      <button type="button" id="db-preview">Preview Query</button>
      <button type="button" id="db-clear">Clear</button>
    </div>
  </section>
  <section class="db-dashboard-card">
    <h2>Scratch Output</h2>
    <pre id="db-result" class="db-dashboard-result">Write a query and preview it here. Execution is intentionally left to your own project-specific tooling.</pre>
  </section>
</div>
<script>
(function () {
  const targetEl = document.getElementById('db-target');
  const queryEl = document.getElementById('db-query');
  const resultEl = document.getElementById('db-result');
  document.getElementById('db-clear').addEventListener('click', function () {
    targetEl.value = '';
    queryEl.value = '';
    resultEl.textContent = 'Write a query and preview it here. Execution is intentionally left to your own project-specific tooling.';
  });
  document.getElementById('db-preview').addEventListener('click', function () {
    const target = targetEl.value.trim() || '(unspecified target)';
    const query = queryEl.value.trim() || '(empty query)';
    resultEl.textContent = [
      'Target: ' + target,
      '',
      'SQL Preview:',
      query
    ].join('\n');
  });
}());
</script>
HTML
        },
        state => {},
        actions => [
            { id => 'source', label => 'View Source', kind => 'builtin', builtin => 'page.source', safe => 1 },
        ],
    );
}

# _shell_dashboard_command()
# Builds a shell-safe command that re-invokes the current dashboard entrypoint.
# Input: none.
# Output: shell command string suitable for generated shell bootstrap helpers.
sub _shell_dashboard_command {
    my ($shell) = @_;
    my $script = File::Spec->rel2abs($0);
    my $repo_lib = File::Spec->rel2abs( File::Spec->catdir( $Bin, '..', 'lib' ) );
    if ( -f File::Spec->catfile( $repo_lib, 'Developer', 'Dashboard.pm' ) ) {
        if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
            return join ' ',
              '&',
              shell_quote_for( $shell, $^X ),
              shell_quote_for( $shell, '-I' . $repo_lib ),
              shell_quote_for( $shell, $script );
        }
        return join ' ',
          shell_quote_for( $shell, $^X ),
          '-I' . shell_quote_for( $shell, $repo_lib ),
          shell_quote_for( $shell, $script );
    }
    return shell_quote_for( $shell, $script ) if $shell eq 'powershell' || $shell eq 'pwsh';
    return shell_quote_for( $shell, $script );
}

# _shell_bootstrap($shell)
# Builds the requested shell bootstrap script with shared navigation helpers and
# shell-specific prompt wiring.
# Input: normalized shell name.
# Output: shell bootstrap script string.
sub _shell_bootstrap {
    my ($shell) = @_;
    my $dashboard_cmd = _shell_dashboard_command($shell);
    my $bootstrap = _shell_navigation_bootstrap($shell) . "\n" . _shell_prompt_bootstrap($shell);
    $bootstrap =~ s/__DASHBOARD_CMD__/$dashboard_cmd/g;
    return $bootstrap;
}

# _shell_navigation_bootstrap()
# Builds the portable shell helpers used by all supported interactive shells for
# bookmark-aware navigation.
# Input: none.
# Output: shell function definitions as a script string.
sub _shell_navigation_bootstrap {
    my ($shell) = @_;
    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return <<'POWERSHELL';
function Get-DashboardTarget {
  param([string[]]$DashboardArgs)
  if (-not $DashboardArgs -or $DashboardArgs.Count -eq 0) {
    return $null
  }
  $target = (__DASHBOARD_CMD__ path resolve $DashboardArgs[0] 2>$null | Select-Object -First 1)
  if (-not $target) {
    $json = __DASHBOARD_CMD__ path locate @DashboardArgs
    if ($LASTEXITCODE -eq 0 -and $json) {
      $parsed = $json | ConvertFrom-Json
      if ($parsed -is [System.Array]) {
        $target = $parsed[0]
      }
      elseif ($parsed) {
        $target = $parsed
      }
    }
  }
  if ($target) {
    return [string]$target
  }
  return $null
}

function cdr {
  param([Parameter(ValueFromRemainingArguments = $true)][string[]]$DashboardArgs)
  $target = Get-DashboardTarget -DashboardArgs $DashboardArgs
  if ($target) {
    Set-Location $target
  }
}

Set-Alias dd_cdr cdr

function which_dir {
  param([Parameter(ValueFromRemainingArguments = $true)][string[]]$DashboardArgs)
  if (-not $DashboardArgs -or $DashboardArgs.Count -eq 0) {
    return
  }
  $resolved = (__DASHBOARD_CMD__ path resolve $DashboardArgs[0] 2>$null | Select-Object -First 1)
  if ($resolved) {
    $resolved
    return
  }
  __DASHBOARD_CMD__ path locate @DashboardArgs
}
POWERSHELL
    }
    return <<'SH';
cdr() {
  target="$(__DASHBOARD_CMD__ path resolve "$1" 2>/dev/null || true)"
  if [ -z "$target" ]; then
    target="$(__DASHBOARD_CMD__ path locate "$@" | perl -MJSON::XS -0777 -ne 'my $a=JSON::XS->new->decode($_); print $a->[0] // q{}')"
  fi
  if [ -n "$target" ]; then
    cd "$target"
  fi
}

dd_cdr() {
  cdr "$@"
}

which_dir() {
  __DASHBOARD_CMD__ path resolve "$1" 2>/dev/null || __DASHBOARD_CMD__ path locate "$@"
}
SH
}

# _shell_prompt_bootstrap($shell)
# Builds the prompt hook snippet for a supported shell so dashboard ps1 can be
# reused outside bash as well.
# Input: normalized shell name.
# Output: shell snippet string.
sub _shell_prompt_bootstrap {
    my ($shell) = @_;
    if ( $shell eq 'bash' ) {
        return <<'BASH';

export PS1='$(__DASHBOARD_CMD__ ps1 --jobs \j --mode compact)'
BASH
    }
    if ( $shell eq 'zsh' ) {
        return <<'ZSH';

autoload -Uz add-zsh-hook
_dd_update_prompt() {
  PS1="$(__DASHBOARD_CMD__ ps1 --jobs ${#jobstates} --mode compact)"
}
add-zsh-hook precmd _dd_update_prompt
_dd_update_prompt
ZSH
    }
    if ( $shell eq 'sh' ) {
        return <<'SH';

_dd_prompt_command() {
  __DASHBOARD_CMD__ ps1 --mode compact
}
PS1='$(_dd_prompt_command)'
export PS1
SH
    }
    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return <<'POWERSHELL';
function prompt {
  __DASHBOARD_CMD__ ps1 --mode compact
}
POWERSHELL
    }
    die "Unsupported shell '$shell'\n";
}

# _resolve_directory_runner($dir)
# Resolves the runnable command file used by one directory-backed custom command.
# Input: command directory path.
# Output: runnable file path string or undef when the directory has no known runner.
sub _resolve_directory_runner {
    my ($dir) = @_;
    return if !defined $dir || $dir eq '' || !-d $dir;
    for my $name (qw(run run.pl run.ps1 run.cmd run.bat run.sh run.bash)) {
        my $path = File::Spec->catfile( $dir, $name );
        my $resolved = resolve_runnable_file($path);
        return $resolved if $resolved;
    }
    return;
}

pod2usage(
    -exitval => 1,
    -verbose => 99,
    -sections => [ qw(NAME SYNOPSIS) ],
);

__END__

=head1 NAME

dashboard - command-line entrypoint for Developer Dashboard

=head1 SYNOPSIS

  dashboard help
  dashboard init
  dashboard update
  dashboard doctor [--fix]
  dashboard ps1 [--jobs N] [--cwd PATH] [--mode compact|extended] [--color]
  dashboard paths
  dashboard path list
  dashboard path resolve <name>
  dashboard path add <name> <path>
  dashboard path del <name>
  dashboard path locate <term...>
  dashboard path project-root
  dashboard of [--print] [--line N] [--editor CMD] <file|scope> [pattern...]
  dashboard open-file [--print] [--line N] [--editor CMD] <file|scope> [pattern...]
  dashboard ticket [ticket-ref]
  dashboard jq [path] [file]
  dashboard yq [path] [file]
  dashboard tomq [path] [file]
  dashboard propq [path] [file]
  dashboard iniq [path] [file]
  dashboard csvq [path] [file]
  dashboard xmlq [path] [file]
  dashboard encode < input.txt
  dashboard decode < token.txt
  dashboard indicator set <name> <label> <icon> <status>
  dashboard indicator list
  dashboard indicator refresh-core [cwd]
  dashboard collector write-result <name> <exit_code>
  dashboard collector status <name>
  dashboard collector list
  dashboard collector job <name>
  dashboard collector output <name>
  dashboard collector inspect <name>
  dashboard collector log
  dashboard collector run <name>
  dashboard collector start <name>
  dashboard collector stop <name>
  dashboard collector restart <name>
  dashboard skills install <git-url>
  dashboard skills uninstall <repo-name>
  dashboard skills update <repo-name>
  dashboard skills list
  dashboard skill <repo-name> <command> [args...]
  dashboard config init
  dashboard config show
  dashboard auth add-user <username> <password>
  dashboard auth list-users
  dashboard auth remove-user <username>
  dashboard page new [id] [title]
  dashboard page save <id>
  dashboard page list
  dashboard page show <id>
  dashboard page encode [id]
  dashboard page decode [token]
  dashboard page urls <id>
  dashboard page render [id|file]
  dashboard page source <id|token>
  dashboard action run <page_id> <action_id>
  dashboard docker compose [--addon NAME] [--mode NAME] [--service NAME] [--project DIR] [--dry-run] <compose-args...>
  dashboard serve [logs [-f] [-n N]|workers <N>] [--host HOST] [--port PORT] [--workers N] [--foreground]
  dashboard stop
  dashboard restart [--host HOST] [--port PORT] [--workers N]
  dashboard shell [bash|zsh|sh|ps|powershell|pwsh]
  dashboard <custom-subcommand> [args...]

=head1 DESCRIPTION

Developer Dashboard provides a project-neutral local developer cockpit with:

=over 4

=item *

saved and transient dashboard pages

=item *

file-backed collectors and indicators

=item *

prompt rendering for C<PS1> and the PowerShell C<prompt> function

=item *

runtime permission auditing and repair through C<dashboard doctor>

=item *

background web-service lifecycle management

=item *

trusted and safer page actions

=item *

config-backed providers and aliases

=item *

project-aware Docker Compose resolution

=item *

user CLI extensions loaded from F<~/.developer-dashboard/cli>

=item *

built-in C<dashboard of> / C<dashboard open-file> resolution for direct files,
C<file:line> references, Perl module names, Java class names, and recursive
pattern matching, including the numbered chooser and default editor fallback

=item *

built-in C<dashboard ticket> tmux session attach/create support through a
private runtime helper staged under F<~/.developer-dashboard/cli/ticket>

=item *

built-in C<dashboard jq> / C<dashboard yq> / C<dashboard tomq> /
C<dashboard propq> parsing for JSON, YAML, TOML, and Java properties input

=item *

built-in C<dashboard iniq> / C<dashboard csvq> / C<dashboard xmlq> parsing 
for INI, CSV, and XML file input

=back

Unknown top-level subcommands can be provided by executable files under
F<~/.developer-dashboard/cli>. For example, C<dashboard foobar a b> will exec
F<~/.developer-dashboard/cli/foobar> with C<a b> as argv, while preserving
stdin, stdout, and stderr.

Per-command hook files can also be placed in either
F<~/.developer-dashboard/cli/E<lt>commandE<gt>> or
F<~/.developer-dashboard/cli/E<lt>commandE<gt>.d>. Executable files in that
directory are run in sorted filename order before the real command runs,
their stdout and stderr stream live to the terminal while still being
accumulated into C<$ENV{RESULT}> as JSON, and non-executable files are
skipped. Built-in commands such as C<dashboard jq> use the same hook
location. A directory-backed custom command may provide its real executable as
F<~/.developer-dashboard/cli/E<lt>commandE<gt>/run>; that runner receives the
final C<$ENV{RESULT}> value after the hook files finish. If a subcommand does
not have a built-in implementation, the real command can be supplied as
F<~/.developer-dashboard/cli/E<lt>commandE<gt>> or
F<~/.developer-dashboard/cli/E<lt>commandE<gt>/run>.

Run C<dashboard> with no arguments for the quick synopsis, or C<dashboard help> for the full POD-backed manual.
