Developer-Dashboard

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

    - removed core seeded-page handling, runtime bootstrap wiring, release
      metadata assertions, and shipped browser tests for the extracted
      optional browser workspaces
    - moved the extracted page assets, dedicated tests, and supporting
      notes out of the core repository tree so the default core tarball no
      longer carries that optional feature set

3.19  2026-04-29
    - fixed dashboard stop and restart so they follow the saved managed
      listener port back to the real serving pid when the live web process
      renames itself into the underlying starman master listener shape
    - fixed minimal Docker lifecycle control so container stop/restart runs no
      longer lose ownership of the active listener after startup

README.md  view on Meta::CPAN

the web service immediately when it is currently stopped; `--host HOST` and
`--port PORT` can steer that auto-start path, and both
`dashboard serve --workers N` and `dashboard restart --workers N` can still
override the worker count for one run
- `dashboard stop` stops both the web service and managed collector loops and,
prints the final lifecycle summary as a terminal table by default or JSON with
`-o json`; on an interactive terminal it also prints the full stop task board
on `stderr` before work starts so each shutdown step becomes visible instead
of silent waiting. The shutdown path now also follows the saved managed
listener port back to the real listener pid when the live web process has
renamed itself into a `starman master` shape, so minimal Docker runs still
stop the actual serving process instead of leaving the listener behind.
Managed collector stop and restart flows also wait for the previous loop to
really die before accepting a replacement, so a slow shutdown does not leave a
stale collector process rewriting loop state while the next restart is proving
the new pid.
- `dashboard stop web` only stops the managed web service
- `dashboard stop collector` only stops managed collector loops
- `dashboard stop collector <name>` only stops the requested managed
collector loop, and collector-name shell completion suggests registered
collector names
- `dashboard restart` stops both, starts configured collector loops again, then
starts the web service, prints the final lifecycle summary as a terminal table
by default or JSON with `-o json`, and only reports success after the
replacement collector loops and web runtime become visible and survive a short
post-ready confirmation window, with the web side still holding a live managed
pid and an accepting listener on the requested port. Restart now also reuses
the saved listener port to recover the real serving pid when the web process
has renamed itself into the underlying `starman master` form, so container
restarts still own and replace the active listener instead of losing control
after startup. On Linux hosts that are also running Developer Dashboard inside
Docker containers, managed stop and restart paths now reject sibling runtime
pids that live in a different Linux pid namespace, so a host-side restart does
not accidentally kill or adopt a container-owned web listener or collector
loop
- `dashboard restart web` only restarts the managed web service
- `dashboard restart collector` only restarts managed collector loops
- `dashboard restart collector <name>` only restarts the requested
collector loop, including an on-demand manual collector by converting it into

doc/integration-test-plan.md  view on Meta::CPAN

- the installed `/ajax/<file>` route streams early output chunks promptly enough to prove browser-visible progress instead of silent buffering
- skill pages that ship `config/routes.json` emit their declared canonical custom ajax paths, while the smart `/ajax/<repo-name>/...` route still works as the parent compatibility resolver and custom paths stay fallback-only before a normal `404`
- layered `config/api.json` files under both runtime roots and installed skills can authorize selected saved `/ajax/...` routes for machine callers, those callers must present matching `X-DD-API-Key` and `X-DD-API-Secret` headers, helper-session auth...
- the built-in `dashboard api` command can list that effective merged registry, hash raw secrets from `--secret` or `--maybe-secret` before saving them, add and remove exact saved ajax routes, and write only to the deepest writable `config/api.json` ...
- non-loopback access produces `401` with an empty body and without a login page until a helper user exists in the active runtime
- under `dashboard serve --ssl`, plain `http://HOST:PORT/...` requests on the public listener return a same-port `307` redirect to `https://HOST:PORT/...`, the generated cert advertises SAN coverage for `localhost`, `127.0.0.1`, and `::1`, and a brow...
- after a helper user exists, non-loopback access produces the helper login page
- helper logout removes both the helper session and the helper account
- `dashboard stop` leaves no active listener on port `7890`
- `dashboard stop` and `dashboard restart` still control the real serving pid
  when the web process has renamed itself into a `starman master` listener
  shape, so container lifecycle checks stay attached to the active listener
- interactive `dashboard stop` and `dashboard restart` runs print the full lifecycle task board on `stderr` before work begins, so managed shutdown and startup waits stay visible instead of looking hung
- `dashboard stop` and `dashboard restart` default to a final terminal table summary, while `-o json` keeps the machine-readable payload
- `dashboard stop web`, `dashboard stop collector`, `dashboard stop collector <name>`, `dashboard restart web`, `dashboard restart collector`, `dashboard restart collector <name>`, `dashboard log`, `dashboard logs`, `dashboard log web`, `dashboard lo...
- runtime stop/restart behavior still works when listener ownership must be
  discovered through `/proc` instead of `ss`
- Linux host lifecycle runs ignore web and collector pids that belong to a
  different pid namespace, so a host-side runtime does not kill or adopt a
  sibling Docker runtime during `dashboard stop` or `dashboard restart`
- `dashboard restart` also succeeds when a listener pid survives the first stop

doc/testing.md  view on Meta::CPAN

- `dashboard serve` collector startup and failure handling, including explicit startup errors and cleanup of already-started loops when a later collector fails
- disabled collector lifecycle handling, including skip-on-start, stop-if-running, and explicit rejection of named starts for collectors with `disable => 1`
- collector watchdog supervision after startup, including automatic restart of unexpectedly-dead loops and explicit `attention_required` state after repeated crashes inside the watchdog window
- collector stall supervision after startup, including automatic restart of a live loop that stops updating its status/output timestamps instead of dying outright
- DD-OOP-LAYERS canonical-path normalization, including a symlinked-home versus canonical-cwd regression that matches macOS `/var/...` and `/private/var/...` alias behaviour
- CLI `dashboard path project-root` assertions compare path identity instead of raw strings, so packaged installs stay green when macOS resolves the same temp repo through `/private/var/...`
- shell-helper `cdr` and `which_dir` assertions also normalize those `/var/...` versus `/private/var/...` aliases, so source-tree and packaged macOS runs do not fail on equivalent canonical paths
- `dashboard web: <host>:<port>` process-title detection
- `pkill` fallback when pid files are stale or missing
- `/proc` listener-pid fallback when minimal Linux containers do not provide `ss`
- saved-port listener fallback for `starman master` listener pids during
  `dashboard stop` and `dashboard restart`, so real serving pids remain under
  runtime control even after the original wrapper title disappears
- Linux pid-namespace isolation for managed web and collector processes, so
  host-side lifecycle checks ignore sibling Docker runtimes that happen to run
  the same Developer Dashboard command names under another namespace
- packaged fallback assertions that stub `_find_web_processes`, so ambient live dashboard processes on the host cannot contaminate the recorded-pid branch during source-tree, tarball, or PAUSE install runs
- `dashboard stop` and `dashboard restart` lifecycle behavior

The extension tests also cover:

lib/Developer/Dashboard.pm  view on Meta::CPAN

override the worker count for one run

=item *

C<dashboard stop> stops both the web service and managed collector loops and,
prints the final lifecycle summary as a terminal table by default or JSON with
C<-o json>; on an interactive terminal it also prints the full stop task board
on C<stderr> before work starts so each shutdown step becomes visible instead
of silent waiting. The shutdown path now also follows the saved managed
listener port back to the real listener pid when the live web process has
renamed itself into a C<starman master> shape, so minimal Docker runs still
stop the actual serving process instead of leaving the listener behind.
Managed collector stop and restart flows also wait for the previous loop to
really die before accepting a replacement, so a slow shutdown does not leave a
stale collector process rewriting loop state while the next restart is proving
the new pid.

=item *

C<dashboard stop web> only stops the managed web service

lib/Developer/Dashboard.pm  view on Meta::CPAN


=item *

C<dashboard restart> stops both, starts configured collector loops again, then
starts the web service, prints the final lifecycle summary as a terminal table
by default or JSON with C<-o json>, and only reports success after the
replacement collector loops and web runtime become visible and survive a short
post-ready confirmation window, with the web side still holding a live managed
pid and an accepting listener on the requested port. Restart now also reuses
the saved listener port to recover the real serving pid when the web process
has renamed itself into the underlying C<starman master> form, so container
restarts still own and replace the active listener instead of losing control
after startup. On Linux hosts that are also running Developer Dashboard inside
Docker containers, managed stop and restart paths now reject sibling runtime
pids that live in a different Linux pid namespace, so a host-side restart does
not accidentally kill or adopt a container-owned web listener or collector
loop

=item *

C<dashboard restart web> only restarts the managed web service

t/05-cli-smoke.t  view on Meta::CPAN

        sleep 0.25;
    }
    ok( $status_response && $status_response->is_success, 'live foreground runtime exposes the system status endpoint' );
    like( decode( 'UTF-8', $status_response->content ), qr/"alias"\s*:\s*"🔑"/, 'live foreground runtime syncs configured collector indicator icons into system status' );
    kill 'TERM', $live_status_pid;
    waitpid( $live_status_pid, 0 );
}
my $dashboard_log_file = File::Spec->catfile( $ENV{HOME}, '.developer-dashboard', 'logs', 'dashboard.log' );
make_path( File::Spec->catdir( $ENV{HOME}, '.developer-dashboard', 'logs' ) );
open my $dashboard_log_fh, '>', $dashboard_log_file or die "Unable to write $dashboard_log_file: $!";
print {$dashboard_log_fh} "starman boot line\nDancer2 boot line\n";
close $dashboard_log_fh;
my $serve_logs = _run("$perl -I'$lib' '$dashboard' serve logs");
like($serve_logs, qr/starman boot line/, 'dashboard serve logs prints the web-service log content');
like($serve_logs, qr/Dancer2 boot line/, 'dashboard serve logs includes Dancer2-side log lines');
my $serve_logs_tail = _run("$perl -I'$lib' '$dashboard' serve logs -n 1");
is($serve_logs_tail, "Dancer2 boot line\n", 'dashboard serve logs -n prints only the requested trailing lines');
{
    require IPC::Open3;
    require Symbol;
    my $stderr_fh = Symbol::gensym();
    my $pid = IPC::Open3::open3( undef, my $stdout_fh, $stderr_fh, $perl, '-I' . $lib, $dashboard, 'serve', 'logs', '-f', '-n', '1' );
    my $first = <$stdout_fh>;
    is( $first, "Dancer2 boot line\n", 'dashboard serve logs -f -n prints the requested trailing lines before following new output' );

t/09-runtime-manager.t  view on Meta::CPAN

    is( $setsid_calls, 1, '_detach_web_process_session calls setsid exactly once on POSIX hosts' );
}

{
    my $state = $manager->_write_web_state();
    is_deeply( $state, {}, '_write_web_state persists an empty hash when no state payload is provided' );
    is_deeply( $manager->web_state, {}, 'web_state reads back the empty-state payload written by _write_web_state' );
    $manager->_cleanup_web_files;
}

$files->write( 'dashboard_log', "starman line\nDancer2 line\n" );
is( $manager->web_log, "starman line\nDancer2 line\n", 'web_log reads the persisted dashboard web-service log output' );
is( $manager->_tail_text( "one\ntwo\nthree\n", 2 ), "two\nthree\n", '_tail_text keeps the requested trailing newline-terminated log lines' );
is( $manager->_tail_text( "one\ntwo\nthree", 2 ), "two\nthree", '_tail_text preserves non-terminated trailing log lines' );
is( $manager->web_log( lines => 1 ), "Dancer2 line\n", 'web_log can return only the last requested number of lines' );
$files->remove('dashboard_log');
is( $manager->web_log, '', 'web_log returns an empty string when the dashboard log file is missing' );
{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_follow_log_file = sub {
        my ( $self, %args ) = @_;
        is( $args{start_pos}, length("follow once\n"), 'web_log follow mode passes the original file byte length into the follow loop so appended lines are not skipped by a seek-to-end race' );

t/09-runtime-manager.t  view on Meta::CPAN

    my $restarted = $manual_manager->restart_target( scope => 'collector', name => 'manual.collector' );
    is( $restarted->{collectors}[0]{name}, 'manual.collector', 'restart_target reports the named manual collector in scoped collector mode' );
    is( $restarted->{collectors}[0]{status}, 'restarted', 'restart_target marks the named manual collector as restarted' );
    ok( grep { $_ eq 'manual.collector' } @{ $manual_runner->{stopped} }, 'restart_target stops an already running named manual collector before restarting it' );
    is( $manual_runner->{started_jobs}[1]{schedule}, 'interval', 'restart_target also converts manual collectors into interval loops for restarts' );
}

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::capture = sub (&) {
        return ( "State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 1024 127.0.0.1:7906 0.0.0.0:* users:((\"starman worker \",pid=123,fd=4),(\"starman master \",pid=456,fd=4))\n", '', 0 );
    };
    local *Developer::Dashboard::RuntimeManager::_is_managed_web = sub {
        my ( undef, $pid ) = @_;
        return $pid == 456 ? 1 : 0;
    };
    is_deeply(
        [ $manager->_managed_listener_pids_for_port(7906) ],
        [456],
        '_managed_listener_pids_for_port filters ss listener pids down to managed dashboard processes',
    );

t/09-runtime-manager.t  view on Meta::CPAN

    };
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
        my ( undef, $port ) = @_;
        return () if $port != 7927;
        $polls++;
        return $polls < 2 ? () : (9912);
    };
    local *Developer::Dashboard::RuntimeManager::_same_pid_namespace = sub { return 1 };
    local *Developer::Dashboard::RuntimeManager::_read_process_title = sub {
        my ( undef, $pid ) = @_;
        return $pid == 9912 ? 'starman master' : 'dashboard web: 127.0.0.1:7927';
    };
    local *Developer::Dashboard::RuntimeManager::_write_web_state = sub {
        my ( undef, $state ) = @_;
        $written_state = { %{$state} };
        return $state;
    };
    local *Developer::Dashboard::FileRegistry::write = sub {
        my ( undef, $name, $content ) = @_;
        $written_pid = $content if $name eq 'web_pid';
        return 1;
    };
    ok(
        $manager->_web_runtime_ready( 8812, 7927 ),
        '_web_runtime_ready adopts the actual listener pid when Starman replaces the startup wrapper process',
    );
    is( $written_pid, "9912\n", '_web_runtime_ready persists the adopted listener pid for later stop and restart flows' );
    is( $written_state->{pid}, 9912, '_web_runtime_ready writes the adopted listener pid into persisted web state' );
    is( $written_state->{process_name}, 'starman master', '_web_runtime_ready refreshes the process title after adopting the listener pid' );
}

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
    local *Developer::Dashboard::RuntimeManager::running_web = sub {
        return {
            pid  => 8808,
            port => 7920,
        };

t/09-runtime-manager.t  view on Meta::CPAN

            port   => 7917,
            status => 'running',
        };
    };
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
        my ( undef, $port ) = @_;
        return $port == 7917 ? (333_333) : ();
    };
    local *Developer::Dashboard::RuntimeManager::_read_process_title = sub {
        my ( undef, $pid ) = @_;
        return $pid == 333_333 ? 'starman master' : undef;
    };
    local *Developer::Dashboard::RuntimeManager::_cleanup_web_files = sub { die "_cleanup_web_files should not run while a saved listener still exists\n" };
    my $running = $manager->running_web;
    is( $running->{pid}, 333_333, 'running_web falls back to the saved listener pid when the real listener no longer keeps the dashboard wrapper title' );
    is( $running->{port}, 7917, 'running_web keeps the persisted port when it resolves the live listener from saved state' );
    is( $running->{process_name}, 'starman master', 'running_web records the actual listener process title when using saved-state listener fallback' );
}

{
    my $late_listener = fork();
    die "fork failed: $!" if !defined $late_listener;
    if ( !$late_listener ) {
        local $SIG{TERM} = 'IGNORE';
        sleep 30;
        POSIX::_exit(0);
    }



( run in 1.725 second using v1.01-cache-2.11-cpan-e93a5daba3e )