Developer-Dashboard
view release on metacpan or search on metacpan
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.338 seconds using v1.01-cache-2.11-cpan-cdf2f3d4e48 )