Developer-Dashboard

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

The same layered runtime config chain and installed-skill config trees can now
ship `config/api.json` files that authorize selected `/ajax/...` routes for
machine-to-machine callers without forcing a helper login form. The schema is a
JSON object keyed by API client name. Each entry must provide a stored SHA-256
hex digest under `secret` plus an `ajax` array of exact saved Ajax route
paths such as `/ajax/stream.txt` or
`/ajax/example-skill/status.json`. When a non-admin remote request targets one
of those registered `/ajax/...` paths, the caller can send
`X-DD-API-Key: NAME` and `X-DD-API-Secret: RAW-SECRET`. Developer Dashboard
hashes the raw secret with SHA-256, compares it to the stored digest, and
executes the saved Ajax handler when they match. Missing or wrong credentials
for a registered API route return `403` with the JSON body
`{"status":"forbidden"}`. Existing helper-session auth still works on the
same saved Ajax routes, so browser workflows and machine callers can coexist on
one handler without adding a second copy of the route. Like the rest of
`DD-OOP-LAYERS`, runtime `config/api.json` files merge from home to the
deepest active child layer, and installed skills contribute their own layered
`config/api.json` fragments for skill-local saved Ajax routes. The built-in
`dashboard api` command is the supported way to inspect or update the writable
runtime layer for that registry.
Saved bookmark Ajax handlers also default to `text/plain` when no explicit

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

- 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
  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

doc/internal-operating-checklist.md  view on Meta::CPAN

4. Use the full system before claiming a blocker.
5. Treat a fix as incomplete until the result is verified and the lesson is captured.
6. Do not wait for a `yes` when the next action is already justified by the task, the current findings, or the repo rules.

## 2. Before Touching Code

1. Read the governing docs and the task-specific docs fully before making assumptions.
2. Confirm the work stays out of `OLD_CODE`.
3. Remove any legacy/company-specific logic from the solution space:
   `Companies House`, `EWF`, `XMLGW`, `CHIPS`, `Tuxedo`, `CHS`, `Grover`,
   `CIDEV`, `PBS`, credentials, and sensitive-data flows do not belong in core.
4. Identify the runtime surfaces affected:
   CLI, browser, collectors, auth, routing, packaging, Windows, Docker,
   layering, prompt, or release workflow.
5. Review related entries in `MISTAKE.md` and `FIXED_BUGS.md` before implementing.

## 3. Design And Implementation Rules

1. Follow TDD.
2. Add or update tests under `t/` first where practical.
3. Keep `dashboard` thin and lazy.

integration/windows/run-qemu-windows-smoke.sh  view on Meta::CPAN

  # Input: the configured Dockur workdir path.
  # Output: deletes the persisted storage, shared, and OEM subdirectories.
  rm -rf \
    "$WINDOWS_DOCKUR_WORKDIR/storage" \
    "$WINDOWS_DOCKUR_WORKDIR/shared" \
    "$WINDOWS_DOCKUR_WORKDIR/oem"
}

run_prepared_qemu_smoke() {
  # Purpose: boot a prepared Windows image and run the Strawberry smoke over SSH.
  # Input: a prepared qcow2 image, SSH credentials, and a built tarball.
  # Output: exits zero only when the in-guest Windows smoke passes.
  if [[ -z "$WINDOWS_IMAGE" ]]; then
    echo "WINDOWS_IMAGE is required and must point to a prepared Windows qcow2 image" >&2
    exit 1
  fi

  if [[ ! -f "$WINDOWS_IMAGE" ]]; then
    echo "Windows image does not exist: $WINDOWS_IMAGE" >&2
    exit 1
  fi

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

The same layered runtime config chain and installed-skill config trees can now
ship C<config/api.json> files that authorize selected C</ajax/...> routes for
machine-to-machine callers without forcing a helper login form. The schema is a
JSON object keyed by API client name. Each entry must provide a stored SHA-256
hex digest under C<secret> plus an C<ajax> array of exact saved Ajax route
paths such as C</ajax/stream.txt> or
C</ajax/example-skill/status.json>. When a non-admin remote request targets one
of those registered C</ajax/...> paths, the caller can send
C<X-DD-API-Key: NAME> and C<X-DD-API-Secret: RAW-SECRET>. Developer Dashboard
hashes the raw secret with SHA-256, compares it to the stored digest, and
executes the saved Ajax handler when they match. Missing or wrong credentials
for a registered API route return C<403> with the JSON body
C<{"status":"forbidden"}>. Existing helper-session auth still works on the
same saved Ajax routes, so browser workflows and machine callers can coexist on
one handler without adding a second copy of the route. Like the rest of
C<DD-OOP-LAYERS>, runtime C<config/api.json> files merge from home to the
deepest active child layer, and installed skills contribute their own layered
C<config/api.json> fragments for skill-local saved Ajax routes. The built-in
C<dashboard api> command is the supported way to inspect or update the writable
runtime layer for that registry.
Saved bookmark Ajax handlers also default to C<text/plain> when no explicit

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

  if (!window.__dashboardAjaxSingletons) window.__dashboardAjaxSingletons = {};
  if (window.__dashboardAjaxSingletons[name]) return;
  window.__dashboardAjaxSingletons[name] = true;
  window.addEventListener('pagehide', function() {
    let url = '/ajax/singleton/stop?singleton=' + encodeURIComponent(name);
    if (navigator.sendBeacon) {
      navigator.sendBeacon(url, '');
      return;
    }
    if (window.fetch) {
      fetch(url, { method: 'POST', keepalive: true, credentials: 'same-origin' }).catch(function () {});
    }
  });
}
function dashboard_target_nodes(target) {
  if (!target) return [];
  if (typeof target === 'string') return Array.prototype.slice.call(document.querySelectorAll(target));
  if (target instanceof Element) return [target];
  if (target.length && typeof target !== 'string') return Array.prototype.slice.call(target);
  return [];
}

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

    if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
      node.value = rendered;
      return;
    }
    node.textContent = rendered;
  });
  return rendered;
}
function fetch_value(url, target, options, formatter) {
  if (!url || !window.fetch) return Promise.resolve('');
  let settings = Object.assign({ credentials: 'same-origin' }, (options && options.fetch) || {});
  return window.fetch(url, settings).then(function(response) {
    if (!response.ok) throw new Error('Request failed with status ' + response.status);
    if (options && options.type === 'json') return response.text();
    return response.text();
  }).then(function(value) {
    return dashboard_write_target(target, value, options || {}, formatter);
  });
}
function dashboard_stream_settings(options) {
  let fetchOptions = (options && options.fetch) || {};
  let method = fetchOptions.method || options.method || 'GET';
  let body = typeof fetchOptions.body !== 'undefined' ? fetchOptions.body : (typeof options.body !== 'undefined' ? options.body : null);
  let headers = fetchOptions.headers || options.headers || {};
  let credentials = fetchOptions.credentials || options.credentials || 'same-origin';
  return {
    method: method,
    body: body,
    headers: headers,
    credentials: credentials
  };
}
function stream_data(url, target, options, formatter) {
  if (!url) return Promise.resolve('');
  if (!window.XMLHttpRequest) return fetch_value(url, target, options, formatter);
  let settings = dashboard_stream_settings(options || {});
  return new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.open(settings.method, url, true);
    xhr.withCredentials = settings.credentials !== 'omit';
    Object.keys(settings.headers || {}).forEach(function(name) {
      xhr.setRequestHeader(name, settings.headers[name]);
    });
    xhr.onprogress = function () {
      dashboard_write_target(target, xhr.responseText, options || {}, formatter);
    };
    xhr.onload = function () {
      if (xhr.status < 200 || xhr.status >= 300) {
        reject(new Error('Request failed with status ' + xhr.status));
        return;

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

    print {$skill_api} qq|{"skill-client":{"secret":"@{[ sha256_hex('skill-secret') ]}","ajax":["/ajax/dancer-route-skill/bar"]}}|;
    close $skill_api or die "Unable to close skill api.json: $!";

    my $api_host = 'dashboard-helper.example:7890';
    my ( $registered_no_api_code, $registered_no_api_type, $registered_no_api_body ) = @{ $app->handle(
        path        => '/ajax/stream.txt',
        query       => 'type=text',
        remote_addr => '127.0.0.1',
        headers     => { host => $api_host },
    ) };
    is( $registered_no_api_code, 403, 'registered remote ajax route rejects requests that omit API credentials' );
    like( $registered_no_api_type, qr/application\/json/, 'registered remote ajax route returns JSON when API credentials are missing' );
    is_deeply( json_decode( decode_body_text($registered_no_api_body) ), { status => 'forbidden' }, 'registered remote ajax route returns the explicit forbidden JSON payload when API credentials are missing' );

    my ( $registered_bad_api_code, undef, $registered_bad_api_body ) = @{ $app->handle(
        path        => '/ajax/stream.txt',
        query       => 'type=text',
        remote_addr => '127.0.0.1',
        headers     => {
            host            => $api_host,
            'x-dd-api-key'    => 'runtime-client',
            'x-dd-api-secret' => 'wrong-secret',
        },

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

    my ( $skill_api_code, $skill_api_type, $skill_api_body ) = @{ $app->handle(
        path        => '/ajax/dancer-route-skill/bar',
        query       => '',
        remote_addr => '127.0.0.1',
        headers     => {
            host              => $api_host,
            'x-dd-api-key'    => 'skill-client',
            'x-dd-api-secret' => 'skill-secret',
        },
    ) };
    is( $skill_api_code, 200, 'installed skill config/api.json can authorize its ajax routes through API credentials' );
    like( $skill_api_type, qr/application\/json/, 'skill API auth keeps the skill ajax response content type' );
    is( decode_body_text( drain_stream_body($skill_api_body) ), qq|{"route":"dancer-top"}\n|, 'installed skill API auth streams the skill ajax response body' );

    my $psgi_app = Developer::Dashboard::Web::DancerApp->build_psgi_app( app => $app );
    Local::PSGITest::test_psgi $psgi_app, sub {
        my ($cb) = @_;
        my $res = $cb->(
            GET(
                'http://127.0.0.1/ajax/stream.txt?type=text',
                Host              => $api_host,



( run in 2.177 seconds using v1.01-cache-2.11-cpan-cdf2f3d4e48 )