Developer-Dashboard

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

Saved bookmark-file routes such as `/app/index` and
`/app/index/action/...` continue to work without that flag. Saved bookmark
editor pages also stay on their named `/app/<id>/edit` and
`/app/<id>` routes when you save from the browser, so editing an
existing bookmark file does not fall back to transient `token=` URLs under the
default deny policy.

`Ajax` helper calls inside saved bookmark `CODE*` blocks should use
an explicit `file => 'name.json'` argument. When a saved page supplies that
name, the helper stores the Ajax Perl code under the saved dashboard ajax tree and emits a
stable saved-bookmark endpoint such as
`/ajax/name.json?type=text`. Skill pages use the same helper contract. Without
extra skill route metadata the generated saved endpoint is namespaced under the
longest matching skill route, for example
`/ajax/example-skill/name.json?type=text` or
`/ajax/example-skill/sub-skill/name.json?type=text`. The runtime config tree
and installed skills can both ship `config/routes.json` to declare canonical
custom paths for normal saved app pages, skill-local app pages, Ajax handlers,
JavaScript assets, CSS assets, and other public assets. The schema is a JSON
object whose keys are the public custom paths and whose values are either one
smart local route string or an object with `to` plus an optional `type`, for
example

README.md  view on Meta::CPAN

Saved bookmark editor and view-source routes also protect literal inline
script content from breaking the browser bootstrap. If a bookmark body
contains HTML such as `/script`, the editor now escapes the inline JSON
assignment used to reload the source text, so the browser keeps the full
bookmark source inside the editor instead of spilling raw text below the page.
Saved browser workspaces can also show a request-specific token form above the
editor whenever the current request uses `{{token}}` placeholders, carrying
those token values across matching placeholders in the same workflow so later
requests can reuse the operator-supplied values without manual copy-and-paste.
Bookmark rendering now emits saved `set_chain_value()` bindings after
the bookmark body HTML, so pages that declare `var endpoints = {}` and then
call helpers from `$(document).ready(...)` receive their saved `/ajax/...`
endpoint URLs without throwing a play-route JavaScript `ReferenceError`.
Bookmark pages now also expose
`fetch_value(url, target, options, formatter)`,
`stream_value(url, target, options, formatter)`, and
`stream_data(url, target, options, formatter)` helpers so a bookmark can bind
saved Ajax endpoints into DOM targets without hand-writing the fetch and
render boilerplate. `stream_data()` and `stream_value()` now use
`XMLHttpRequest` progress events for browser-visible incremental updates, so
a saved `/ajax/...` endpoint that prints early output updates the DOM before
the request finishes. Those helpers support plain text, JSON, and HTML output
modes, and the saved Ajax endpoint bindings now run after the page declares
its endpoint root object, so `$(document).ready(...)` callbacks can call
helpers such as `fetch_value(endpoints.foo, '#foo')` on first render.
Saved browser workspaces that render response inspection panels should place
their Response Body and Response Headers tabs below the response `pre` box so
the main response payload stays visible while the tabbed details remain
reachable without jumping away from the current result.

## User CLI Extensions

Unknown top-level subcommands can be provided by executable files under
the current working directory's `./.developer-dashboard/cli` first, then the
nearest git-backed project runtime `./.developer-dashboard/cli` when it is a

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

16. Kill one managed collector loop after startup, confirm the watchdog restarts it automatically, and verify `dashboard collector status <name>` records watchdog restart counters/timestamps. Kill it repeatedly until the watchdog limit is exceeded, t...
17. Exercise page create/save/show/encode/decode/render/source flows inside the fake bookmark directory.
18. Exercise builtin action execution.
19. For Windows-targeted changes, run `integration/windows/run-strawberry-smoke.ps1 -UseInstallBootstrap -BootstrapScript <checkout install.ps1>` so the guest validates the same streamed `Invoke-Expression` bootstrap shape that operators use with `ir...
20. Exercise docker compose dry-run resolution against a temporary project.
21. Start the installed web service.
22. Confirm exact-loopback access reaches the editor page in Chromium.
23. Confirm the browser can render a saved fake-project bookmark page from the fake project bookmark directory.
24. Confirm the browser inserts sorted rendered `nav/*.tt` bookmark fragments between the top chrome and the main page body.
25. Confirm the browser top-right status strip shows configured collector icons, not collector names, that UTF-8 icons such as `🐳` and `💰` are visibly rendered, and that renamed collectors no longer leave stale managed indicators behind.
26. Confirm an installed saved bookmark page can declare `var endpoints = {};`, then use `fetch_value()` and `stream_value()` from `$(document).ready(...)` against saved `/ajax/<file>` routes without inline-script ordering failures or browser console...
27. Confirm an installed long-running saved `/ajax/<file>` route starts streaming the first output chunks promptly instead of buffering until the worker exits.
28. Confirm an installed skill page that ships `config/routes.json` emits the declared canonical custom ajax path, that the custom path resolves, that the smart `/ajax/<repo-name>/...` route still resolves for the same handler, and that a route-level...
29. Confirm non-loopback self-access returns `401` with an empty body and without a login form before any helper user exists in the active runtime.
30. Add a helper user for the outsider browser flow, then confirm non-loopback self-access reaches the helper login page in Chromium.
31. Log in as a helper through the HTTP helper flow.
32. Confirm helper page chrome shows `Logout`.
33. Log out and confirm the helper account is removed.
34. Restart the installed runtime from the extracted tarball tree and confirm the web service comes back.
35. Stop the runtime and confirm the web service is gone.

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

- a killed managed collector loop is restarted automatically by the watchdog, and repeated crash loops eventually surface `watchdog_attention_required` in `dashboard collector status <name>` instead of going silent
- a live managed collector loop that stops updating its status or completion timestamps is treated as stalled, recycled automatically by the watchdog, and reported explicitly in `dashboard collector status <name>` instead of sitting silent forever
- `dashboard collector log` prints aggregated collector transcripts, `dashboard collector log <name>` prints the named collector transcript, and configured collectors that have not run yet report an explicit no-log message instead of blank output
- TT-backed collector icons render from stdout JSON and stay rendered through later config-sync reads instead of reverting to raw `[% ... %]` text
- the web service serves the root editor on `127.0.0.1:7890`
- the browser can load both the editor and a saved fake-project bookmark page from the fake project bookmark directory
- the browser sees sorted shared `nav/*.tt` fragments above the main page body on that fake-project bookmark page
- the browser top-right status strip shows configured collector icons and does not leave stale renamed collector indicators behind
- nested `DD-OOP-LAYERS` collector prompts do not let a child-layer placeholder `missing` state override a healthy inherited parent-layer collector indicator when the child config adds no collector override
- under `DD-OOP-LAYERS`, `dashboard path add` writes only the new child-layer alias delta into the deepest child `config/config.json` instead of copying inherited parent config domains into that file
- bookmark pages can use `fetch_value()`, `stream_value()`, and `stream_data()` helpers against saved `/ajax/...` endpoints on first render
- 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

doc/static-file-serving.md  view on Meta::CPAN

<img src="/others/logo.png">
<link rel="icon" href="/others/favicon.ico">
<script src="/others/config.json" type="application/json"></script>
<img src="/others/example-skill/icon.svg">
<img src="/others/example-skill/sub-skill/path/file.txt">
```

### Skill-local Ajax Endpoints
```html
<script>
var endpoints = {};
</script>
```

Skill bookmark CODE blocks can publish stable endpoints such as:

```perl
CODE1: Ajax jvar => 'endpoints.status', file => 'status', code => q{
print "ok\n";
};
```

When that page lives inside `~/.developer-dashboard/skills/example-skill/dashboards/...`,
the browser-facing endpoint becomes:

```text
/ajax/example-skill/status?type=text
```

Nested child skills extend that pattern:

```text
/ajax/example-skill/sub-skill/status?type=text
```

doc/testing.md  view on Meta::CPAN

  --expect-ajax-body 123 \
  --expect-dom-fragment '<span class="display">123</span>'
```

For a skill page that declares `config/routes.json`, assert the canonical
custom ajax path rather than the default smart `/ajax/<repo-name>/...` path:

```bash
integration/browser/run-bookmark-browser-smoke.pl \
  --bookmark-file ~/.developer-dashboard/skills/example-skill/dashboards/index \
  --expect-page-fragment "set_chain_value(endpoints,'status','/v1/status')" \
  --expect-ajax-path /v1/status \
  --expect-ajax-body '{"status":"ok"}'
```

For long-running saved bookmark Ajax handlers that would otherwise survive a
browser refresh, prefer `Ajax(..., singleton => 'NAME', ...)`. The runtime will
rename the Perl worker to `dashboard ajax: NAME`, terminate the older matching
Perl stream before it starts the refreshed one, and also tear down matching
singleton workers during `dashboard stop`, `dashboard restart`, and browser
`pagehide` cleanup beacons. For browser streaming checks, use `stream_data()`

doc/update-and-release.md  view on Meta::CPAN

for failing or missing checks. `dashboard shell` now emits shell-specific
bootstrap for bash, zsh, POSIX `sh`, and PowerShell `ps`, so release
validation should cover whichever interactive shell bootstrap a new feature
touches. PowerShell verification should check the generated `prompt`
function rather than looking for a POSIX `PS1` export.
The browser top-right status strip should also show the configured collector
icon instead of the collector name, and a collector rename should remove the
old managed indicator from both `/system/status` and `dashboard ps1`. Verify
that UTF-8 icons such as `🐳` and `💰` are actually visible in the browser
chrome, not just present in `/system/status` JSON. For bookmark Ajax helper
pages that declare `var endpoints = {};`, verify the saved `set_chain_value()`
bindings run after that declaration so `$(document).ready(...)` helper calls
populate the DOM without a console `ReferenceError`.
Permission-sensitive changes should also verify that `dashboard doctor`
reports insecure older or home-runtime paths before repair and returns clean
after `--fix`.

Render prompt in extended colored mode:

```bash
perl -Ilib bin/dashboard ps1 --jobs 1 --mode extended --color

doc/update-and-release.md  view on Meta::CPAN


That helper keeps the Windows VM flow rerunnable by loading a reusable env
file, rebuilding the latest tarball when needed, and then delegating to the
checked-in QEMU launcher. The supported Windows runtime baseline is PowerShell
plus Strawberry Perl. Git Bash is optional. Scoop is optional. They are setup
helpers only.

For browser-facing bookmark Ajax changes, also run a real browser smoke that
verifies saved Ajax bindings are emitted before inline page scripts and that
helpers such as `fetch_value()`, `stream_value()`, and `stream_data()` can
populate the DOM from saved `/ajax/...` endpoints without manual bootstrap
ordering fixes or whole-response buffering.

For seeded UI workspaces, run a browser smoke from a fresh project-local
runtime and verify the real rendered DOM includes the expected controls and
navigation rather than only checking the saved bookmark source text.

Command-output capture is implemented with `Capture::Tiny` `capture`, with exit codes returned from the capture block. The core runtime does not currently make outbound HTTP client requests.

## Coverage Verification

integration/blank-env/run-integration.pl  view on Meta::CPAN

    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'legacy-ajax' ),
        <<'BOOKMARK'
TITLE: Legacy Ajax
:--------------------------------------------------------------------------------:
BOOKMARK: legacy-ajax
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.project.endpoint', type => 'text', file => 'project-endpoint.json', code => q{
print "saved-start\n";
warn "saved-warn\n";
system 'sh', '-c', 'printf "saved-child-out\n"; printf "saved-child-err\n" >&2';
die "saved-die\n";
};
BOOKMARK
    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'legacy-ajax-stream' ),
        <<'BOOKMARK'

integration/blank-env/run-integration.pl  view on Meta::CPAN


    my $project_dom = _run_browser_dom( 'browser fake project page', 'http://127.0.0.1:7890/app/project-home', user_data_dir => $profile );
    _assert_match( $project_dom, qr/project-marker/, 'browser renders fake project bookmark page' );
    _assert_match( $project_dom, qr/Fake Project Home/, 'browser renders fake project bookmark content' );
    _assert_match( $project_dom, qr/dashboard-nav-items/, 'browser renders shared nav container on fake project bookmark page' );
    _assert_match( $project_dom, qr/Alpha Nav Current/, 'browser renders shared nav TT output against the outer page path' );
    _assert_match( $project_dom, qr{<div id="nav-beta">/app/project-home / /app/project-home</div>}, 'browser exposes current_page through env and env.runtime_context for shared nav TT fragments' );
    _assert( index( $project_dom, 'nav-alpha' ) < index( $project_dom, 'nav-beta' ), 'browser renders shared nav bookmark fragments in sorted filename order' );
    _assert( index( $project_dom, 'dashboard-nav-items' ) < index( $project_dom, 'project-marker' ), 'browser renders shared nav fragments before the main page body' );
    my $legacy_ajax_page = _run_shell( 'curl legacy ajax saved page', q{curl -fsS http://127.0.0.1:7890/app/legacy-ajax} );
    _assert_match( $legacy_ajax_page->{stdout}, qr{/ajax/project-endpoint\.json\?type=text}, 'saved bookmark Ajax renders a stable file-backed ajax endpoint by default' );
    my $legacy_ajax_saved = _run_shell( 'curl saved bookmark ajax endpoint', q{curl -fsS 'http://127.0.0.1:7890/ajax/project-endpoint.json?type=text'} );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-start/, 'saved bookmark ajax endpoint streams direct perl stdout' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-warn/, 'saved bookmark ajax endpoint streams perl stderr warnings' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-child-out/, 'saved bookmark ajax endpoint streams child stdout' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-child-err/, 'saved bookmark ajax endpoint streams child stderr' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-die/, 'saved bookmark ajax endpoint streams uncaught perl die output' );
    my $legacy_ajax_stream_page = _run_shell( 'curl legacy ajax stream saved page', q{curl -fsS http://127.0.0.1:7890/app/legacy-ajax-stream} );
    _assert_match( $legacy_ajax_stream_page->{stdout}, qr{/ajax/project-stream\.txt\?type=text}, 'saved bookmark ajax stream page renders a stable default text ajax endpoint' );
    my $legacy_ajax_stream = _capture_stream_prefix(
        'curl saved bookmark ajax stream endpoint',
        q{curl --no-buffer -fsS 'http://127.0.0.1:7890/ajax/project-stream.txt'},
        expected_chunks => [ 'stream1', 'stream2' ],
        timeout         => 4,
    );
    _assert( @{ $legacy_ajax_stream->{events} || [] } >= 2, 'saved bookmark ajax stream endpoint produced multiple early chunks before process exit' );
    _assert( ( $legacy_ajax_stream->{events}[0]{at} || 99 ) < 1.5, 'saved bookmark ajax stream endpoint flushes the first chunk before the long-running ajax loop finishes' );
    _assert( ( $legacy_ajax_stream->{events}[1]{at} || 99 ) < 2.5, 'saved bookmark ajax stream endpoint keeps flushing later chunks during the long-running ajax loop' );

    my $container_ip = _trim( _run_shell( 'container ip', q{hostname -I | awk '{print $1}'} )->{stdout} );
    _assert( $container_ip ne '', 'container ip discovered for helper-access path' );

    my $helper_root_disabled = _run_shell(
        'curl helper root before helper user exists',
        'curl -sS -o /tmp/helper-root.html -w \'%{http_code}\' http://' . $container_ip . ':7890/'
    );
    _assert_match( $helper_root_disabled->{stdout}, qr/^401$/, 'non-loopback self-access stays unauthorized before any helper user exists' );
    _assert( _read_text('/tmp/helper-root.html') eq q{}, 'outsider bootstrap response keeps the body empty before any helper user exists' );

integration/browser/run-bookmark-browser-smoke.pl  view on Meta::CPAN

Example 1:

  perl integration/browser/run-bookmark-browser-smoke.pl --keep-temp

Keep the temporary project, home, and browser profile directories around for debugging after a failure.

Example 2:

  perl integration/browser/run-bookmark-browser-smoke.pl --bookmark-file /tmp/sample.page --expect-ajax-path '/ajax/test?type=text' --expect-ajax-body 'ok'

Exercise one saved Ajax endpoint as part of the bookmark smoke.

Example 3:

  perl integration/browser/run-bookmark-browser-smoke.pl --bookmark-file /tmp/sample.page --expect-page-fragment 'Hello' --expect-dom-fragment 'Hello'

Point the smoke runner at one explicit bookmark file and assert both raw page and browser DOM output.

Example 4:

  perl integration/browser/run-bookmark-browser-smoke.pl

integration/windows/run-strawberry-smoke.ps1  view on Meta::CPAN


    foreach ($candidate in $candidates) {
        if (Test-Path $candidate) {
            return $candidate
        }
    }

    return $null
}

# Purpose: wait until the dashboard HTTP endpoint responds.
# Input: target URL string.
# Output: returns when the URL responds with a non-5xx code or throws on timeout.
function Wait-HttpOk {
    param([Parameter(Mandatory = $true)][string]$Url)

    $deadline = (Get-Date).AddSeconds(20)
    while ((Get-Date) -lt $deadline) {
        try {
            $response = Invoke-WebRequest -UseBasicParsing -Uri $Url -TimeoutSec 2
            if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {

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

Saved bookmark-file routes such as C</app/index> and
C</app/index/action/...> continue to work without that flag. Saved bookmark
editor pages also stay on their named C</app/E<lt>idE<gt>/edit> and
C</app/E<lt>idE<gt>> routes when you save from the browser, so editing an
existing bookmark file does not fall back to transient C<token=> URLs under the
default deny policy.

C<Ajax> helper calls inside saved bookmark C<CODE*> blocks should use
an explicit C<file =E<gt> 'name.json'> argument. When a saved page supplies that
name, the helper stores the Ajax Perl code under the saved dashboard ajax tree and emits a
stable saved-bookmark endpoint such as
C</ajax/name.json?type=text>. Skill pages use the same helper contract. Without
extra skill route metadata the generated saved endpoint is namespaced under the
longest matching skill route, for example
C</ajax/example-skill/name.json?type=text> or
C</ajax/example-skill/sub-skill/name.json?type=text>. The runtime config tree
and installed skills can both ship C<config/routes.json> to declare canonical
custom paths for normal saved app pages, skill-local app pages, Ajax handlers,
JavaScript assets, CSS assets, and other public assets. The schema is a JSON
object whose keys are the public custom paths and whose values are either one
smart local route string or an object with C<to> plus an optional C<type>, for
example

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

Saved bookmark editor and view-source routes also protect literal inline
script content from breaking the browser bootstrap. If a bookmark body
contains HTML such as C</script>, the editor now escapes the inline JSON
assignment used to reload the source text, so the browser keeps the full
bookmark source inside the editor instead of spilling raw text below the page.
Saved browser workspaces can also show a request-specific token form above the
editor whenever the current request uses C<{{token}}> placeholders, carrying
those token values across matching placeholders in the same workflow so later
requests can reuse the operator-supplied values without manual copy-and-paste.
Bookmark rendering now emits saved C<set_chain_value()> bindings after
the bookmark body HTML, so pages that declare C<var endpoints = {}> and then
call helpers from C<$(document).ready(...)> receive their saved C</ajax/...>
endpoint URLs without throwing a play-route JavaScript C<ReferenceError>.
Bookmark pages now also expose
C<fetch_value(url, target, options, formatter)>,
C<stream_value(url, target, options, formatter)>, and
C<stream_data(url, target, options, formatter)> helpers so a bookmark can bind
saved Ajax endpoints into DOM targets without hand-writing the fetch and
render boilerplate. C<stream_data()> and C<stream_value()> now use
C<XMLHttpRequest> progress events for browser-visible incremental updates, so
a saved C</ajax/...> endpoint that prints early output updates the DOM before
the request finishes. Those helpers support plain text, JSON, and HTML output
modes, and the saved Ajax endpoint bindings now run after the page declares
its endpoint root object, so C<$(document).ready(...)> callbacks can call
helpers such as C<fetch_value(endpoints.foo, '#foo')> on first render.
Saved browser workspaces that render response inspection panels should place
their Response Body and Response Headers tabs below the response C<pre> box so
the main response payload stays visible while the tabbed details remain
reachable without jumping away from the current result.

=head2 User CLI Extensions

Unknown top-level subcommands can be provided by executable files under
the current working directory's F<./.developer-dashboard/cli> first, then the
nearest git-backed project runtime F<./.developer-dashboard/cli> when it is a

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

        next if $path !~ m{(?:/proc/self/fd|/dev/fd)/(\d+)\z};
        my $fd = $1 + 0;
        next if $seen{$fd}++;
        push @fds, $fd;
    }
    return sort { $a <=> $b } @fds;
}

# _descriptor_is_inherited_pipe($fd)
# Returns whether one descriptor currently points at an inherited capture or
# IPC endpoint that a detached collector child should close after stdio has
# been redirected.
# Input: descriptor integer.
# Output: boolean true when the descriptor target is an inherited pipe,
# socketpair, or anonymous kernel handle.
sub _descriptor_is_inherited_pipe {
    my ( $self, $fd, %args ) = @_;
    return 0 if !defined $fd || $fd !~ /^\d+$/;
    my $proc_target = readlink("/proc/self/fd/$fd");
    my $dev_target  = readlink("/dev/fd/$fd");
    my $target = defined $proc_target ? $proc_target : $dev_target;

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

        next if $path !~ m{(?:/proc/self/fd|/dev/fd)/(\d+)\z};
        my $fd = $1 + 0;
        next if $seen{$fd}++;
        push @fds, $fd;
    }
    return sort { $a <=> $b } @fds;
}

# _descriptor_is_inherited_pipe($fd)
# Returns whether one descriptor currently points at an inherited capture or
# IPC endpoint that a detached runtime child should close after stdio has been
# redirected.
# Input: descriptor integer.
# Output: boolean true when the descriptor target is an inherited pipe,
# socketpair, or anonymous kernel handle.
sub _descriptor_is_inherited_pipe {
    my ( $self, $fd, %args ) = @_;
    return 0 if !defined $fd || $fd !~ /^\d+$/;
    my $proc_target = readlink("/proc/self/fd/$fd");
    my $dev_target  = readlink("/dev/fd/$fd");
    my $target = defined $proc_target ? $proc_target : $dev_target;

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

=item *

C<stream_data(url, target, options, formatter)>

=back

Bookmark pages can also use the built-in C</js/jquery.js> compatibility shim
for C<$>, C<$(document).ready(...)>, and C<$.ajax(...)>.

For normal saved runtime bookmarks, C<Ajax(file =E<gt> 'name', ...)> can create
stable saved Ajax endpoints. That capability is tied to the saved runtime
bookmark path. Skill bookmarks render through the skill route surface, so do
not assume stable saved C</ajax/E<lt>fileE<gt>> handlers there unless you have
tested that path explicitly.

=head1 NAV AND DASHBOARD-WIDE CLI

Normal runtime bookmarks support shared C<nav/*.tt> fragments above non-nav
saved pages. Skill pages auto-load C<dashboards/nav/*> into
C</app/E<lt>repo-nameE<gt>> and C</app/E<lt>repo-nameE<gt>/...> routes, and
the same installed skill nav fragments are also rendered above normal saved

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


  my $app = Developer::Dashboard::Web::App->new(
      auth     => $auth,
      pages    => $pages,
      sessions => $sessions,
  );

=head1 DESCRIPTION

This module handles the browser-facing dashboard routes, helper login flow,
page rendering modes, and page/action execution endpoints. It also provides
static file serving for JavaScript, CSS, and other assets from the public
directory structure (~/.developer-dashboard/dashboard/public/{js,css,others}).

=head1 METHODS

=head2 new, handle

Construct and dispatch the local web application.

=head2 _serve_static_file($type, $filename)

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


Input: $type (js, css, or others), $filename (requested filename).
Output: MIME type string suitable for Content-Type header.

Supports: JS, CSS, JSON, XML, HTML, SVG, PNG, JPEG, GIF, WebP, ICO, and others.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module is the main route backend for the browser application. It handles login and logout, saved and transient page render/source/edit routes, status endpoints, saved Ajax endpoints, and the auth checks that decide whether a request is local adm...

=head1 WHY IT EXISTS

It exists because the dashboard browser surface is large and security-sensitive. Centralizing route behavior, auth gating, saved-page handling, Ajax endpoints, layered C<config/api.json> machine auth, and response shaping keeps the product behavior c...

=head1 WHEN TO USE

Use this file when changing browser routes, helper login behavior, page render/source/edit flows, saved Ajax endpoints, or the runtime JSON and HTML responses for dashboard workspaces.

=head1 HOW TO USE

Construct it with the action runner, auth service, config service, page store, prompt, page resolver, page runtime, and session store, then hand it to the Dancer adapter or PSGI bootstrap. Route-specific behavior belongs here rather than in the trans...

=head1 WHAT USES IT

It is used by C<Developer::Dashboard::Web::DancerApp>, by C<app.psgi>, by the CLI web server wrapper, and by the broad web/browser regression suite that covers routes, auth, Ajax, and workspace behavior.

=head1 EXAMPLES

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

    my $url = $base ? $base . $query : $query;
    return {
        token   => $token,
        url     => { tokenised => $url, app => $args{app} || $url },
        forward => [ $path => { token => $token->{raw}, type => $type } ],
        html    => sprintf( q{<a href="%s" target="%s">%s</a>}, $url, ( $args{target} || '_blank' ), ( $args{label} || 'Click Here' ) ),
    };
}

# Ajax(%args)
# Prints a older config-binding script for an encoded ajax endpoint.
# Input: jvar, type, optional file/singleton names, and optional code values.
# Output: hide marker string.
sub Ajax {
    my %args = @_;
    die "jvar is required" if !$args{jvar};
    my $type = $args{type} || 'text';
    my $context = ref($AJAX_CONTEXT) eq 'HASH' ? $AJAX_CONTEXT : {};
    if ( ( ( $context->{source} || '' ) eq 'saved' || ( $context->{source} || '' ) eq 'skill' ) && ( $context->{page_id} || '' ) ne '' ) {
        my $file = $args{file} || '';
        if ( $file eq '' && !( $context->{allow_transient_urls} || 0 ) ) {

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

=head2 zip, unzip, acmdx, Ajax, __cmdx, _cmdx, _cmdp

Encode and decode token payloads and generate older-style ajax links. Saved
bookmark Ajax file handlers are stored under the dashboards ajax tree as
executable files so the web runtime can run them as real processes.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module keeps the older bookmark and Ajax helper compatibility surface alive. It builds tokenised URLs, saved Ajax endpoints, and helper snippets such as C<Ajax()> while routing the actual encoding work through the modern codec module.

=head1 WHY IT EXISTS

It exists because older bookmarks still expect the historical helper names and URL-building patterns. Keeping those wrappers in one module preserves compatibility without forcing newer runtime code to keep re-implementing the old API directly.

=head1 WHEN TO USE

Use this file when changing older Ajax helper behavior, saved Ajax file validation, token URL generation, or the compatibility wrappers that older bookmark instructions still reference.

=head1 HOW TO USE

t/03-web-app.t  view on Meta::CPAN

like($body4, qr/Right Click Copy &amp; Share or Bookmark This Page/, 'legacy render includes top chrome share link');
unlike($body4, qr/\{legacy-welcome:[^}]+\}/, 'top chrome does not dump shell prompt project context');
unlike($body4, qr/\[\w{3}\s+\w{3}/, 'top chrome does not dump shell prompt timestamps');
like($body4, qr/id="status-on-top"/, 'legacy render includes old top-status container');
like($body4, qr/class="user-name-and-icon"/, 'legacy render includes top-right user marker');
like($body4, qr/id="status-server"/, 'legacy render includes top-right server marker');
like($body4, qr/10\.20\.30\.40/, 'legacy render includes machine ip instead of request host');
like($body4, qr/id="status-datetime"/, 'legacy render includes live-updated date-time marker');

my ($status_code, $status_type, $status_body) = @{ $app->handle(path => '/system/status', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($status_code, 200, 'legacy status endpoint route ok');
like($status_type, qr/application\/json/, 'legacy status endpoint returns json');
like($status_body, qr/"array"\s*:/, 'legacy status endpoint returns array payload');
$config->save_global(
    {
        collectors => [
            {
                name      => 'vpn',
                code      => 'return 0;',
                cwd       => 'home',
                indicator => {
                    icon => '🔑',
                },
            },
        ],
    }
);
my ($status_icon_code, undef, $status_icon_body) = @{ $app->handle(path => '/system/status', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($status_icon_code, 200, 'legacy status endpoint still responds after syncing config-backed collector indicators');
like(decode('UTF-8', $status_icon_body), qr/"alias"\s*:\s*"🔑"/, 'legacy status endpoint exposes configured collector indicator icons instead of collector names');
like($app->_prompt_summary, qr/🔑/, 'page top-right prompt summary prefers the configured collector indicator icon');
$config->save_global_web_settings( no_editor => 1 );
my ($readonly_render_code, undef, $readonly_render_body) = @{ $app->handle(path => '/app/welcome', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($readonly_render_code, 200, 'no-editor mode still renders saved pages');
unlike($readonly_render_body, qr/id="share-url"/, 'no-editor mode hides the share link from render views');
unlike($readonly_render_body, qr/id="view-source-url"/, 'no-editor mode hides the view-source link from render views');
unlike($readonly_render_body, qr/id="play-button"/, 'no-editor mode hides the play button from render views');
my ($readonly_source_code, $readonly_source_type, $readonly_source_body) = @{ $app->handle(path => '/app/welcome/source', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($readonly_source_code, 403, 'no-editor mode blocks saved page source routes');
like($readonly_source_type, qr/text\/plain/, 'no-editor blocked source route returns plain text');

t/03-web-app.t  view on Meta::CPAN

$config->save_global_web_settings( no_editor => 0 );
$config->save_global_web_settings( no_indicators => 1 );
my ($noind_render_code, undef, $noind_render_body) = @{ $app->handle(path => '/app/welcome', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($noind_render_code, 200, 'no-indicators mode still renders saved pages');
unlike($noind_render_body, qr/id="status-on-top"/, 'no-indicators mode hides the top-right indicator strip');
unlike($noind_render_body, qr/id="status-datetime"/, 'no-indicators mode hides the top-right date-time marker');
unlike($noind_render_body, qr/id="status-server"/, 'no-indicators mode hides the top-right server marker');
unlike($noind_render_body, qr/class="user-name-and-icon"/, 'no-indicators mode hides the top-right username marker');
like($app->_prompt_summary, qr/🔑/, 'no-indicators mode does not change prompt-summary data generation');
my ($noind_status_code, undef, $noind_status_body) = @{ $app->handle(path => '/system/status', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($noind_status_code, 200, 'no-indicators mode keeps the status endpoint available');
like(decode('UTF-8', $noind_status_body), qr/"alias"\s*:\s*"🔑"/, 'no-indicators mode keeps status endpoint indicator payloads intact');
$config->save_global_web_settings( no_indicators => 0 );
$config->save_global(
    {
        collectors => [
            {
                name      => 'vpn-renamed',
                code      => 'return 0;',
                cwd       => 'home',
                indicator => {
                    icon => '🔑',
                },
            },
        ],
    }
);
my ($status_rename_code, undef, $status_rename_body) = @{ $app->handle(path => '/system/status', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($status_rename_code, 200, 'legacy status endpoint still responds after a collector rename');
unlike($status_rename_body, qr/"prog"\s*:\s*"vpn"/, 'legacy status endpoint removes stale managed collector indicators after a collector rename');
like($status_rename_body, qr/"prog"\s*:\s*"vpn-renamed"/, 'legacy status endpoint keeps the renamed collector indicator');

my $legacy_token = $store->encode_page($legacy_page);
my ($code5, undef, $body5) = @{ $app->handle(path => '/', query => "mode=render&token=$legacy_token", remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code5, 200, 'legacy transient page route ok');
like($body5, qr/<div>Runtime<\/div>/, 'legacy transient pages execute CODE blocks through the same runtime');

my ($code5b, undef, $body5b) = @{ $app->handle(path => '/app/legacy-welcome', query => 'name=Michael', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code5b, 200, 'legacy /app route with query params ok');
like($body5b, qr/Hello Michael/, 'legacy /app bookmark merges request params into stash');

my $legacy_ajax_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
TITLE: Legacy Ajax
:--------------------------------------------------------------------------------:
BOOKMARK: legacy-ajax
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'json', code => q{
  print j { ok => 1 };
}, file => 'demo.json';
PAGE
$store->save_page($legacy_ajax_page);

my ($code6, undef, $body6) = @{ $app->handle(path => '/app/legacy-ajax', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code6, 200, 'legacy /app route renders bookmark');
like($body6, qr/set_chain_value\(configs,'demo\.endpoint','\/ajax\/demo\.json\?type=json/, 'legacy Ajax helper injects a saved bookmark ajax endpoint when a file is supplied');
ok( -f File::Spec->catfile( $paths->dashboards_root, 'ajax', 'demo.json' ), 'legacy Ajax helper stores the saved bookmark ajax code under the dashboards ajax tree' );

my ($code7, $type7, $body7) = @{ $app->handle(path => '/ajax/demo.json', query => "type=json", remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code7, 200, 'legacy ajax endpoint executes through the saved bookmark ajax file route');
like($type7, qr/application\/json/, 'legacy ajax endpoint returns json type');
like(drain_stream_body($body7), qr/"ok"\s*:\s*1/, 'legacy ajax endpoint returns encoded payload output as a stream');

my $existing_ajax_dir = File::Spec->catdir( $paths->dashboards_root, 'ajax' );
make_path($existing_ajax_dir);
my $existing_ajax_file = File::Spec->catfile( $existing_ajax_dir, 'existing.sh' );
open my $existing_ajax_fh, '>', $existing_ajax_file or die $!;
print {$existing_ajax_fh} "#!/bin/sh\nprintf 'existing-out\\n'\nprintf 'existing-err\\n' >&2\n";
close $existing_ajax_fh;
chmod 0700, $existing_ajax_file or die $!;

my $legacy_existing_ajax_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
TITLE: Legacy Existing Ajax
:--------------------------------------------------------------------------------:
BOOKMARK: legacy-ajax-existing
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'existing.sh';
PAGE
$store->save_page($legacy_existing_ajax_page);

my ($code7b, undef, $body7b) = @{ $app->handle(path => '/app/legacy-ajax-existing', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code7b, 200, 'legacy /app route renders bookmark that points at an existing ajax file');
like($body7b, qr/set_chain_value\(configs,'demo\.endpoint','\/ajax\/existing\.sh\?type=text/, 'legacy Ajax helper injects an existing saved bookmark ajax endpoint when only a file is supplied');
my $bootstrap_pos = index( $body7b, 'function set_chain_value' );
my $binding_pos   = index( $body7b, q{set_chain_value(configs,'demo.endpoint','/ajax/existing.sh?type=text'} );
ok( $bootstrap_pos > -1 && $binding_pos > $bootstrap_pos, 'legacy bootstrap is defined before saved Ajax bindings run in render mode' );

my ($code7d, $type7d, $body7d) = @{ $app->handle(path => '/ajax/existing.sh', query => "type=text", remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code7d, 200, 'saved bookmark ajax file route executes an existing ajax executable from the dashboards ajax tree');
like($type7d, qr/text\/plain/, 'existing ajax executable returns text type');
my $existing_stream = drain_stream_body($body7d);
like($existing_stream, qr/existing-out/, 'existing ajax executable streams stdout');
like($existing_stream, qr/existing-err/, 'existing ajax executable streams stderr');

my ($jquery_code, $jquery_type, $jquery_body) = @{ $app->handle(path => '/js/jquery.js', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };

t/03-web-app.t  view on Meta::CPAN

:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'foo.bar', file => 'foobar', code => q{
print 123
};
PAGE
$store->save_page($legacy_jquery_ajax_page);

my ($jquery_page_code, undef, $jquery_page_body) = @{ $app->handle(path => '/app/test-jquery-ajax', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($jquery_page_code, 200, 'legacy jquery ajax bookmark route renders');
like($jquery_page_body, qr{<script src="/js/jquery\.js"></script>}, 'legacy jquery ajax bookmark keeps the jquery helper script tag');
like($jquery_page_body, qr{set_chain_value\(foo,'bar','/ajax/foobar\?type=text'\)}, 'legacy jquery ajax bookmark binds foo.bar to the saved ajax endpoint with default text type');
my ($jquery_ajax_code, $jquery_ajax_type, $jquery_ajax_body) = @{ $app->handle(path => '/ajax/foobar', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($jquery_ajax_code, 200, 'legacy jquery ajax bookmark saved endpoint is executable');
like($jquery_ajax_type, qr/text\/plain/, 'legacy jquery ajax bookmark saved endpoint defaults to text content type when no type is supplied');
is(drain_stream_body($jquery_ajax_body), '123', 'legacy jquery ajax bookmark saved endpoint returns the code output');

my $fetch_stream_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: fetch-stream-helpers
:--------------------------------------------------------------------------------:
HTML: <script src="/js/jquery.js"></script>
<span id="foo"></span><br>
<div id="bar"></div>
<span id="mike"></span><br>
<script>
var endpoints = {};
$(document).ready(function () {
  fetch_value(endpoints.foo, '#foo');
  stream_value(endpoints.bar, '#bar', { type: 'text' });
  fetch_value(endpoints.mike, '#mike', { type: 'json' }, function (value) {
    return value.ok > 0 ? 'OK' : 'Error';
  });
});
</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'endpoints.foo', file => 'foo', code => q{
  print "This is foo echo";
};
:--------------------------------------------------------------------------------:
CODE2: Ajax jvar => 'endpoints.bar', file => 'bar', singleton => 'BAR', code => q{
  print "bar-one\n";
  print "bar-two\n";
};
:--------------------------------------------------------------------------------:
CODE3: Ajax jvar => 'endpoints.mike', file => 'mike', type => 'json', code => q{
  use Developer::Dashboard::DataHelper qw( j );
  print j { ok => 1 };
};
PAGE
$store->save_page($fetch_stream_page);
my ($fetch_stream_code, undef, $fetch_stream_body) = @{ $app->handle(path => '/app/fetch-stream-helpers', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($fetch_stream_code, 200, 'legacy bookmark with fetch_value and stream_value helpers renders');
like($fetch_stream_body, qr/function fetch_value\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes fetch_value helper');
like($fetch_stream_body, qr/function stream_value\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes stream_value helper');
like($fetch_stream_body, qr/function stream_data\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes stream_data helper');
like($fetch_stream_body, qr/new XMLHttpRequest\(\)/, 'legacy bookmark streaming helper uses XMLHttpRequest for progressive browser updates');
like($fetch_stream_body, qr/xhr\.onprogress = function \(\)/, 'legacy bookmark streaming helper updates targets from incremental ajax progress events');
my $foo_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'foo','/ajax/foo?type=text'});
my $bar_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'bar','/ajax/bar?type=text&singleton=BAR'});
my $mike_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'mike','/ajax/mike?type=json'});
my $endpoints_decl_pos = index($fetch_stream_body, q{var endpoints = {};});
my $fetch_call_pos = index($fetch_stream_body, q{fetch_value(endpoints.foo, '#foo');});
ok($foo_bind_pos > -1 && $bar_bind_pos > -1 && $mike_bind_pos > -1, 'legacy bookmark render includes all saved Ajax endpoint bindings for fetch_value and stream_value');
ok($endpoints_decl_pos > -1, 'legacy bookmark render keeps the caller endpoint variable declaration');
ok($foo_bind_pos > $endpoints_decl_pos && $bar_bind_pos > $endpoints_decl_pos && $mike_bind_pos > $endpoints_decl_pos, 'saved Ajax endpoint bindings render after the caller declares the endpoint root object');
ok($fetch_call_pos > -1, 'legacy bookmark render keeps the inline fetch helper call');
like($fetch_stream_body, qr/dashboard_ajax_singleton_cleanup\('BAR'\)/, 'legacy bookmark render keeps singleton cleanup bindings for stream_value pages');

my $stream_data_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: stream-data-helper
:--------------------------------------------------------------------------------:
HTML: <script src="/js/jquery.js"></script>
<script>var foo = {};
$(document).ready(function () {
  stream_data(foo.bar, '.display');

t/03-web-app.t  view on Meta::CPAN

    while (1) {
      print 123;
      sleep 1;
    }
};
PAGE
$store->save_page($stream_data_page);
my ($stream_data_code, undef, $stream_data_body) = @{ $app->handle(path => '/app/stream-data-helper', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($stream_data_code, 200, 'legacy bookmark with stream_data helper renders');
like($stream_data_body, qr{stream_data\(foo\.bar, '\.display'\);}, 'legacy bookmark render keeps the inline stream_data helper call');
like($stream_data_body, qr{set_chain_value\(foo,'bar','/ajax/foobar\?type=text&singleton=FOOBAR'\)}, 'legacy bookmark render binds stream_data ajax endpoint before browser execution');

{
    open my $fh, '>', $store->page_file('legacy-forward') or die $!;
    print {$fh} '/ajax/demo.json?type=text';
    close $fh;
}
my ($code8, $type8, $body8) = @{ $app->handle(path => '/app/legacy-forward', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code8, 200, 'legacy /app saved-url forwarding works');
like($type8, qr/text\/plain/, 'forwarded saved-url bookmark preserves content type');
like(drain_stream_body($body8), qr/"ok"\s*:\s*1/, 'forwarded saved-url bookmark reaches ajax payload through the stream response');

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

        exec $perl, '-I' . $lib, $dashboard, 'serve', '--foreground', '--host', '127.0.0.1', '--port', $live_status_port;
        die "Unable to exec live dashboard serve: $!";
    }
    my $status_ua = LWP::UserAgent->new( timeout => 5 );
    my $status_response;
    for ( 1 .. _startup_probe_attempts() ) {
        $status_response = $status_ua->get("http://127.0.0.1:$live_status_port/system/status");
        last if $status_response->is_success;
        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");

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

    unlike( $noind_render_body, qr/id="status-on-top"/, 'no-indicators live server hides the top-right indicator strip' );
    unlike( $noind_render_body, qr/id="status-datetime"/, 'no-indicators live server hides the top-right date-time marker' );
    unlike( $noind_render_body, qr/id="status-server"/, 'no-indicators live server hides the top-right server marker' );
    unlike( $noind_render_body, qr/class="user-name-and-icon"/, 'no-indicators live server hides the top-right user marker' );
    my $noind_status_response;
    for ( 1 .. 240 ) {
        $noind_status_response = $noind_ua->get("http://127.0.0.1:$noind_port/system/status");
        last if $noind_status_response->code == 200;
        sleep 0.25;
    }
    is( $noind_status_response->code, 200, 'no-indicators live server keeps the status endpoint available' );
    like( decode( 'UTF-8', $noind_status_response->decoded_content ), qr/"array"\s*:/, 'no-indicators live server still exposes status payload data' );
    my $noind_ps1 = _run_in_home( $noind_home, "$perl -I'$lib' '$dashboard' ps1 --jobs 0" );
    like( $noind_ps1, qr/\S/, 'no-indicators mode does not blank the terminal prompt output' );
    my $noind_config_file = File::Spec->catfile( $noind_home, '.developer-dashboard', 'config', 'config.json' );
    open my $noind_config_fh, '<', $noind_config_file or die "Unable to read $noind_config_file: $!";
    my $noind_config = do { local $/; <$noind_config_fh> };
    close $noind_config_fh;
    like( $noind_config, qr/"web"\s*:\s*\{[\s\S]*"no_indicators"\s*:\s*1/s, 'dashboard serve --no-indicator persists no_indicators in config' );
    my $noind_restart = json_decode( _run_in_home( $noind_home, "$perl -I'$lib' '$dashboard' restart -o json --host 127.0.0.1 --port $noind_port" ) );
    ok( $noind_restart->{web_pid}, 'dashboard restart keeps managing the no-indicators web service' );

t/08-web-update-coverage.t  view on Meta::CPAN

    like( $ajax_bad_file_type, qr/text\/plain/, 'legacy ajax invalid saved-file route returns plain text' );
    like( $ajax_bad_file_body, qr/invalid parent traversal/, 'legacy ajax invalid saved-file route returns the validation error text' );
}

{
    my $streaming_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-stream
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'stream.txt', code => q{
  print "first\n";
  print "second\n";
};
PAGE
    $store->save_page($streaming_page);
    my ( undef, undef, undef ) = @{ $app->handle( path => '/app/ajax-stream', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my ( $ajax_stream_code, $ajax_stream_type, $ajax_stream_body ) = @{ $app->handle( path => '/ajax/stream.txt', query => 'type=text', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    is( $ajax_stream_code, 200, 'legacy ajax saved-file route responds successfully for streaming output' );
    like( $ajax_stream_type, qr/text\/plain/, 'legacy ajax saved-file route keeps the requested content type for streaming output' );
    is( drain_stream_body($ajax_stream_body), "first\nsecond\n", 'legacy ajax saved-file route streams raw printed output without page buffering' );
}

{
    my $process_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-process
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'process-endpoint.json', code => q{
print "perl-start\n";
warn "perl-warn\n";
system 'sh', '-c', 'printf "child-out\n"; printf "child-err\n" >&2';
die "perl-die\n";
};
PAGE
    $store->save_page($process_page);
    my ( undef, undef, undef ) = @{ $app->handle( path => '/app/ajax-process', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my ( $ajax_process_code, $ajax_process_type, $ajax_process_body ) = @{ $app->handle( path => '/ajax/process-endpoint.json', query => 'type=text', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my $ajax_process_output = drain_stream_body($ajax_process_body);
    is( $ajax_process_code, 200, 'legacy ajax saved-file process route responds successfully for mixed stdout and stderr output' );
    like( $ajax_process_type, qr/text\/plain/, 'legacy ajax saved-file process route keeps the requested content type' );
    like( $ajax_process_output, qr/perl-start/, 'legacy ajax saved-file process route streams direct perl stdout' );
    like( $ajax_process_output, qr/perl-warn/, 'legacy ajax saved-file process route streams perl stderr warnings' );
    like( $ajax_process_output, qr/child-out/, 'legacy ajax saved-file process route streams child process stdout' );
    like( $ajax_process_output, qr/child-err/, 'legacy ajax saved-file process route streams child process stderr' );
    like( $ajax_process_output, qr/perl-die/, 'legacy ajax saved-file process route streams uncaught perl die output' );
}

{
    my $singleton_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-singleton
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', singleton => 'FOOBAR', file => 'singleton-endpoint.txt', code => q{
print "$0\n";
};
PAGE
    $store->save_page($singleton_page);
    my ( undef, undef, $singleton_page_body ) = @{ $app->handle( path => '/app/ajax-singleton', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    like( $singleton_page_body, qr{/ajax/singleton-endpoint\.txt\?type=text&singleton=FOOBAR}, 'saved bookmark Ajax page emits the singleton query parameter in the generated ajax url' );
    like( $singleton_page_body, qr/dashboard_ajax_singleton_cleanup\('FOOBAR'\)/, 'saved bookmark Ajax page registers browser lifecycle cleanup for singleton-managed workers' );
    my ( $ajax_singleton_code, undef, $ajax_singleton_body ) = @{ $app->handle( path => '/ajax/singleton-endpoint.txt', query => 'type=text&singleton=FOOBAR', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my $ajax_singleton_output = drain_stream_body($ajax_singleton_body);
    is( $ajax_singleton_code, 200, 'legacy ajax saved-file route responds successfully for singleton-managed requests' );
    like( $ajax_singleton_output, qr/^dashboard ajax: FOOBAR$/m, 'legacy ajax saved-file route renames singleton-managed Perl workers before streaming output' );
}

{
    my @patterns;
    {
        no warnings 'redefine';
        local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub {

t/08-web-update-coverage.t  view on Meta::CPAN

        'singleton stop route and direct response target the matching saved ajax worker process title',
    );
}

{
    my $shebang_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-shebang
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'script-runner', code => qq{#!/bin/sh\nprintf 'shell-out\\n'\nprintf 'shell-err\\n' >&2\n};
PAGE
    $store->save_page($shebang_page);
    my ( undef, undef, undef ) = @{ $app->handle( path => '/app/ajax-shebang', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my ( $ajax_shebang_code, undef, $ajax_shebang_body ) = @{ $app->handle( path => '/ajax/script-runner', query => 'type=text', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my $ajax_shebang_output = drain_stream_body($ajax_shebang_body);
    is( $ajax_shebang_code, 200, 'legacy ajax saved-file route executes shebang scripts directly' );
    like( $ajax_shebang_output, qr/shell-out/, 'legacy ajax saved-file route streams direct executable stdout' );
    like( $ajax_shebang_output, qr/shell-err/, 'legacy ajax saved-file route streams direct executable stderr' );
}

t/08-web-update-coverage.t  view on Meta::CPAN

        headers     => {
            host            => $api_host,
            'x-dd-api-key'    => 'runtime-client',
            'x-dd-api-secret' => 'wrong-secret',
        },
    ) };
    is( $registered_bad_api_code, 403, 'registered remote ajax route rejects invalid API secrets' );
    is_deeply( json_decode( decode_body_text($registered_bad_api_body) ), { status => 'forbidden' }, 'registered remote ajax route keeps the forbidden JSON payload for invalid API secrets' );

    my ( $unregistered_remote_code, undef, $unregistered_remote_body ) = @{ $app->handle(
        path        => '/ajax/process-endpoint.json',
        query       => 'type=text',
        remote_addr => '127.0.0.1',
        headers     => { host => $api_host },
    ) };
    is( $unregistered_remote_code, 401, 'unregistered remote ajax routes still follow the existing helper-auth flow' );
    is( $unregistered_remote_body, '', 'unregistered remote ajax routes stay silent before helper users exist' );

    my ( $runtime_api_code, $runtime_api_type, $runtime_api_body ) = @{ $app->handle(
        path        => '/ajax/stream.txt',
        query       => 'type=text',

t/12-legacy-helper-coverage.t  view on Meta::CPAN

like( __cmdx( perl => 'print 1;' ), qr/base64 -d \| gunzip/, '__cmdx builds a shell decode pipeline' );
my @cmdx = _cmdx( perl => 'print 1;' );
is_deeply( [ @cmdx[ 0, 1 ] ], [ 'perl', '-e' ], '_cmdx returns shell tuple metadata' );
my @cmdp = _cmdp( perl => 'print 1;' );
is( $cmdp[1], 'perl', '_cmdp returns pipeline metadata' );
my $ajax_url = acmdx( type => 'json', code => 'print qq{{}};' );
like( $ajax_url->{url}{tokenised}, qr{^/ajax\?token=}, 'acmdx builds a tokenised ajax url' );
my $ajax_singleton_url = acmdx( type => 'text', code => 'print qq{ok};', singleton => 'TRANSIENT' );
like( $ajax_singleton_url->{url}{tokenised}, qr/[?&]singleton=TRANSIENT/, 'acmdx carries the optional singleton value into transient ajax urls' );
my ( $ajax_stdout, undef, $ajax_result ) = capture {
    return Ajax( jvar => 'configs.coverage.endpoint', code => 'print qq{{}};' );
};
like( $ajax_stdout, qr/set_chain_value/, 'Ajax prints the legacy config-binding script' );
is( $ajax_result, 'HIDE-THIS', 'Ajax returns the legacy hide marker' );
my ( $ajax_singleton_stdout, undef, $ajax_singleton_result ) = capture {
    return Ajax( jvar => 'configs.coverage.endpoint', code => 'print qq{{}};', singleton => 'TRANSIENT' );
};
like( $ajax_singleton_stdout, qr/[?&]singleton=TRANSIENT/, 'Ajax carries the optional singleton value into transient ajax bindings' );
is( $ajax_singleton_result, 'HIDE-THIS', 'Ajax still returns the hide marker when a transient singleton is supplied' );
{
    local $Developer::Dashboard::Zipper::AJAX_CONTEXT = {
        source               => 'saved',
        page_id              => 'coverage-page',
        runtime_root         => $paths->runtime_root,
        allow_transient_urls => 0,
    };

t/15-release-metadata.t  view on Meta::CPAN

    unlike( $doc, qr/dashboard skill example-skill/, 'skill authoring docs no longer describe the removed singular dispatcher' );
    like( $doc, qr{~/.developer-dashboard/skills/<repo-name>/|F<~/.developer-dashboard/skills/E<lt>repo-nameE<gt>/>}, 'skill authoring docs describe the isolated skill root' );
    like( $doc, qr/DD-OOP-LAYERS.*skill|skill.*DD-OOP-LAYERS/is, 'skill authoring docs describe layered skill lookup through DD-OOP-LAYERS' );
    like( $doc, qr/deepest.*shadow|shadow.*deepest|deepest matching repo name/is, 'skill authoring docs describe deepest-layer skill shadowing' );
    like( $doc, qr/cli\/<command>\.d|cli\/E<lt>commandE<gt>\.d/, 'skill authoring docs explain skill hook directories' );
    like( $doc, qr/dashboards\//, 'skill authoring docs explain skill bookmark storage' );
    like( $doc, qr{/app/<repo-name>|/app/E<lt>repo-nameE<gt>|/skill/<repo-name>/bookmarks/<id>|/skill/E<lt>repo-nameE<gt>/bookmarks/E<lt>idE<gt>}, 'skill authoring docs explain skill bookmark routes' );
    like( $doc, qr{/ajax/<repo-name>/|/ajax/E<lt>repo-nameE<gt>/|/js/<repo-name>/|/css/<repo-name>/|/others/<repo-name>/}, 'skill authoring docs explain skill-local ajax and public asset routes' );
    like( $doc, qr/TITLE:.*BOOKMARK:.*HTML:.*CODE1:/s, 'skill authoring docs explain bookmark section syntax' );
    like( $doc, qr/fetch_value\(|stream_value\(|stream_data\(/, 'skill authoring docs explain bookmark browser helpers' );
    like( $doc, qr/Ajax\(file\s*=>\s*'name'|C<Ajax\(file =E<gt> 'name'/, 'skill authoring docs explain saved Ajax endpoints' );
    like( $doc, qr/nav\/\*\.tt|nav\/foo\.tt/, 'skill authoring docs explain nav bookmark structure' );
    like( $doc, qr{~/.developer-dashboard/cli/<command>\.d|~/.developer-dashboard/cli/E<lt>commandE<gt>\.d}, 'skill authoring docs explain dashboard-wide custom CLI hooks' );
    like( $doc, qr/DEVELOPER_DASHBOARD_SKILL_ROOT/, 'skill authoring docs explain the skill command environment' );
    like( $doc, qr/LAST_RESULT/, 'skill authoring docs explain previous-hook payloads' );
    like( $doc, qr/RESULT_FILE|LAST_RESULT_FILE/, 'skill authoring docs explain file-backed hook result overflow handling' );
    like( $doc, qr/\[\[STOP\]\]/, 'skill authoring docs explain explicit hook stop markers' );
    like( $doc, qr/_example-skill|_<repo-name>|_E<lt>repo-nameE<gt>|_something/, 'skill authoring docs explain underscored skill config merge keys' );
    like( $doc, qr/aptfile/, 'skill authoring docs explain isolated apt dependency installation' );
    like( $doc, qr/cpanfile/, 'skill authoring docs explain isolated dependency installation' );
    like( $doc, qr/config\/docker/, 'skill authoring docs explain skill docker roots' );

t/20-skill-web-routes.t  view on Meta::CPAN

    is( $missing_segments_page->{meta}{render_route}, '/app/route-skill', 'missing smart route segments keep the canonical smart-routed render alias' );
}

my $ajax_page = $app->handle(
    path        => '/app/route-skill/ajax-demo',
    method      => 'GET',
    headers     => { host => '127.0.0.1' },
    remote_addr => '127.0.0.1',
);
is( $ajax_page->[0], 200, 'skill ajax demo page route returns success' );
like( $ajax_page->[2], qr{set_chain_value\(endpoints,'bar','/v1/route-skill/bar'\)}, 'skill page binds saved ajax helper to the canonical custom skill route' );

my $skill_alias_ajax = $app->handle(
    path        => '/v1/route-skill/bar',
    method      => 'GET',
    headers     => { host => '127.0.0.1' },
    remote_addr => '127.0.0.1',
);
is( $skill_alias_ajax->[0], 200, 'custom skill ajax alias route returns success' );
is( $skill_alias_ajax->[1], 'application/json; charset=utf-8', 'custom skill ajax alias route applies its default json content type' );
is( _drain_stream_body( $skill_alias_ajax->[2] ), qq|{"route":"bar"}\n|, 'custom skill ajax alias route streams the skill handler output' );

t/20-skill-web-routes.t  view on Meta::CPAN

is( $nested_custom_page->[0], 200, 'nested custom skill app route returns success' );
like( $nested_custom_page->[2], qr/Nested Skill Foo/, 'nested custom skill app route renders the nested skill bookmark' );

my $nested_ajax_page = $app->handle(
    path        => '/app/route-skill/def/ajax-demo',
    method      => 'GET',
    headers     => { host => '127.0.0.1' },
    remote_addr => '127.0.0.1',
);
is( $nested_ajax_page->[0], 200, 'nested skill ajax demo page route returns success' );
like( $nested_ajax_page->[2], qr{set_chain_value\(endpoints,'nested','/v1/route-skill/nested'\)}, 'nested skill page binds saved ajax helper to the nested canonical custom route' );

my $nested_alias_ajax = $app->handle(
    path        => '/v1/route-skill/nested',
    method      => 'GET',
    headers     => { host => '127.0.0.1' },
    remote_addr => '127.0.0.1',
);
is( $nested_alias_ajax->[0], 200, 'nested custom ajax alias route returns success' );
is( $nested_alias_ajax->[1], 'text/html; charset=utf-8', 'nested custom ajax alias route applies its default html content type' );
is( _drain_stream_body( $nested_alias_ajax->[2] ), "<p>nested skill ajax route</p>\n", 'nested custom ajax alias route streams the nested skill handler output' );

t/20-skill-web-routes.t  view on Meta::CPAN

    _write_file(
        File::Spec->catfile( 'dashboards', 'ajax-demo' ),
        <<'BOOKMARK',
TITLE: Skill Ajax Demo
:--------------------------------------------------------------------------------:
BOOKMARK: ajax-demo
:--------------------------------------------------------------------------------:
HTML:
<div id="ajax-demo">Ajax Demo</div>
<script>
var endpoints = {};
</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'endpoints.bar', file => 'bar', type => 'json', code => q{
print qq|{"route":"bar"}\n|;
};
BOOKMARK
        0644,
    );
    _write_file(
        File::Spec->catfile( 'config', 'routes.json' ),
        sprintf(
            <<'JSON',
{

t/20-skill-web-routes.t  view on Meta::CPAN

    _write_file(
        File::Spec->catfile( 'skills', 'def', 'dashboards', 'ajax-demo' ),
        <<'BOOKMARK',
TITLE: Nested Skill Ajax Demo
:--------------------------------------------------------------------------------:
BOOKMARK: ajax-demo
:--------------------------------------------------------------------------------:
HTML:
<div id="nested-ajax-demo">Nested Ajax Demo</div>
<script>
var endpoints = {};
</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'endpoints.nested', file => 'nested', code => q{
print "<p>nested skill ajax route</p>\n";
};
BOOKMARK
        0644,
    );
    _write_file(
        File::Spec->catfile( 'skills', 'def', 'config', 'routes.json' ),
        sprintf(
            <<'JSON',
{



( run in 2.031 seconds using v1.01-cache-2.11-cpan-524268b4103 )