: null}
- {page === 'overview' ? (
+ {page === 'developers' ? : page === 'overview' ? (
<>
{}).constructor;
+describe('developer reference', () => {
+ it('renders public English/LTR documentation and accessible copy results', () => {
+ const html = renderToStaticMarkup();
+ expect(html).toContain('lang="en" dir="ltr"'); expect(html).toContain('aria-live="polite"');
+ for (const key of PHASE_1_EDITABLE_SETTING_KEYS) expect(html).toContain(key);
+ for (const key of Object.keys(REFERENCE_SNIPPETS)) expect(html).toContain(`aria-label="Copy ${key} example"`);
+ });
+ it('uses retained settings/query/fragment for section links', () => {
+ expect(referenceSectionUrl('/render/APP/Node/Node?page=settings&x=a&x=b#retained','writes')).toBe('/render/APP/Node/Node?page=settings&x=a&x=b&view=developers§ion=writes#retained');
+ });
+ it('discovers capabilities without executing settings writes', async () => {
+ const request = vi.fn(async ({ action }) => action === 'SHOW_ACTIONS' ? ['UPDATE_NODE_SETTINGS'] : true);
+ await new AsyncFunction('qdnRequest', REFERENCE_SNIPPETS.capabilities)(request);
+ expect(request.mock.calls.map(([r]) => r.action)).toEqual(['SHOW_ACTIONS', 'IS_USING_PUBLIC_NODE']);
+ });
+ it('rejects a failed read envelope rather than treating an error as peer data', async () => {
+ const request = vi.fn(async ({ action }) => action === 'GET_NODE_STATUS' ? { height: 1 } : { ok: false, status: 403 });
+ await expect(new AsyncFunction('qdnRequest', REFERENCE_SNIPPETS.reads)(request)).rejects.toThrow('403');
+ });
+ it('keeps write and restart examples inert until called and does not retry rejection', async () => {
+ const request = vi.fn(async () => { throw new Error('Denied'); });
+ const save = await new AsyncFunction('qdnRequest', REFERENCE_SNIPPETS.save + '\nreturn saveReviewedPatch;')(request);
+ const restart = await new AsyncFunction('qdnRequest', REFERENCE_SNIPPETS.restart + '\nreturn requestRestart;')(request);
+ expect(request).not.toHaveBeenCalled();
+ await expect(save({ apiDocumentationEnabled: false })).rejects.toThrow('Denied');
+ expect(request).toHaveBeenCalledExactlyOnceWith({ action: 'UPDATE_NODE_SETTINGS', settings: { apiDocumentationEnabled: false } });
+ await expect(restart()).rejects.toThrow('Denied');expect(request).toHaveBeenCalledTimes(2);
+ });
+ it('matches documented writable metadata shapes to the app normalizer', () => {
+ const value = { type: 'boolean', restartRequired: true };
+ const entry = { key: 'apiDocumentationEnabled', value };
+ expect(normalizeSettingsMetadata({ writable: { apiDocumentationEnabled: value } })?.writable).toEqual({ apiDocumentationEnabled: value });
+ expect(normalizeSettingsMetadata({ writable: { entry: [entry] } })?.writable).toEqual({ apiDocumentationEnabled: value });
+ expect(normalizeSettingsMetadata({ writable: [entry] })?.writable).toEqual({});
+ });
+ it('pins documented unit examples and boundaries to the real parsers', async () => {
+ const result = await new AsyncFunction(REFERENCE_SNIPPETS.units + '\nreturn [storageBytes, retentionMilliseconds];')();
+ expect(result).toEqual([parseGigabytesToBytes('2.5'), parseHoursToMilliseconds('24')]);
+ expect(parseGigabytesToBytes('0.5')).toBeNull();expect(parseHoursToMilliseconds('1.0001')).toBeNull();
+ });
+});
diff --git a/src/Reference.tsx b/src/Reference.tsx
new file mode 100644
index 0000000..7a270b8
--- /dev/null
+++ b/src/Reference.tsx
@@ -0,0 +1,111 @@
+import { useState } from 'react';
+import { copyTextToClipboard } from './clipboard';
+import { ReferenceNavigation } from './ReferenceNavigation';
+import { NODE_READ_PATHS, SETTINGS_BRIDGE_ACTIONS } from './nodeContract';
+import { AUTO_UPDATE_MODE_OPTIONS, CHAT_RETENTION_HOUR_MS, PHASE_1_EDITABLE_SETTING_KEYS, STORAGE_CAPACITY_GIGABYTE_BYTES, STORAGE_POLICY_OPTIONS, TRANSPORT_SELECTION_OPTIONS } from './settingsEditor';
+
+export const REFERENCE_SNIPPETS = {
+ capabilities: `const actions = await qdnRequest({ action: 'SHOW_ACTIONS' });
+const available = new Set(Array.isArray(actions) ? actions : []);
+const required = ${JSON.stringify(SETTINGS_BRIDGE_ACTIONS)};
+const missing = required.filter(action => !available.has(action));
+const isPublic = await qdnRequest({ action: 'IS_USING_PUBLIC_NODE' });
+// Missing actions or public mode mean read-only. Availability is not approval.
+// Do not infer write authority from a URL, a settings field, or WHICH_UI.`,
+ reads: `const status = await qdnRequest({ action: 'GET_NODE_STATUS' });
+const response = await qdnRequest({
+ action: 'FETCH_NODE_API', path: '/peers', maxBytes: 1024 * 1024,
+});
+// FETCH_NODE_API returns an HTTP envelope; check it before reading data.
+if (!response.ok) throw new Error('Peer lookup failed: HTTP ' + response.status);
+const peers = Array.isArray(response.data) ? response.data : [];
+// Optional diagnostics can be unavailable; an empty fallback is not proof
+// that Core has no peers or that every transport is healthy.`,
+ metadata: `const response = await qdnRequest({ action: 'GET_NODE_SETTINGS_METADATA' });
+const metadata = response && typeof response === 'object' && 'data' in response
+ ? response.data : response;
+// Normalize writable: either { key: { type, restartRequired } },
+// { entry: [{ key, value: { type, restartRequired } }] }.
+// Node's normalizeSettingsMetadata() handles these shapes.
+// Intersect with the app's editable allowlist before rendering inputs.
+const appEditableKeys = ${JSON.stringify(PHASE_1_EDITABLE_SETTING_KEYS)};`,
+ units: `const bytesPerGB = ${STORAGE_CAPACITY_GIGABYTE_BYTES}; // decimal GB, not GiB
+const millisecondsPerHour = ${CHAT_RETENTION_HOUR_MS};
+const storageBytes = 2.5 * bytesPerGB;
+const retentionMilliseconds = 24 * millisecondsPerHour;
+// Node uses parseGigabytesToBytes / parseHoursToMilliseconds:
+// require at least 1 displayed unit and at most 3 decimal places.
+// Conversion rounds to an integer and rejects unsafe values.`,
+ save: `// Define the operation; copying this example does not call it.
+async function saveReviewedPatch(reviewedPatch) {
+ // Use only changed, validated, metadata-writable and app-allowed keys.
+ // Home presents its own current/proposed values and selected node approval.
+ const result = await qdnRequest({
+ action: 'UPDATE_NODE_SETTINGS', settings: reviewedPatch,
+ });
+ // result: { saved, applied, updated, removed, restartRequired }
+ // Re-read settings and metadata. A saved restart-required value can still
+ // differ from what restart-dependent services use. No automatic retry on ambiguous failure.
+ return result;
+}
+// This is not a revision/CAS store: do not add expectedRevision.`,
+ restart: `// A separate explicit user action, never part of copying or saving.
+async function requestRestart() {
+ return qdnRequest({ action: 'RESTART_NODE' });
+}
+// Home asks to restart the selected Core. { accepted: true } acknowledges
+// the request, not a completed restart or a healthy/synchronized node.
+// Expect temporary disconnection; refresh status/metadata after it returns.`,
+} as const;
+
+export function Reference() {
+ const [copied, setCopied] = useState('');
+ async function copy(key: string, value: string, button: HTMLButtonElement) {
+ setCopied(await copyTextToClipboard(value) ? key : 'unavailable');
+ button.focus({ preventScroll: true });
+ }
+ return
+
Developers
+
Qortium Core inspection and settings through Qortium Home.
+
+
{copied === 'unavailable' ? 'Clipboard unavailable. Select the code and copy it manually.' : copied ? `Copied ${copied} example.` : 'Code examples can be selected for manual copying.'}
+
+
+
Host and authority
+
Node is a Qortium-only app published as APP/Node/Node. It inspects the active Qortium Core route selected in Home; it is not a Qortal node manager. The public app bundle and this reference contain no live node settings, credentials or wallet data.
+
Use SHOW_ACTIONS and IS_USING_PUBLIC_NODE to discover the current host. Node’s write UI requires an update action, a node not reported as public, and writable metadata for each app-allowed field. An unknown public-node flag is not authority: Home independently requires a trusted local or authenticated route and a per-request approval. Switching node or API-key trust while approving invalidates the write.
+
Home owns the connection and credentials. Apps must not ask users to paste API keys or private keys, embed them in QDN assets, or use a direct browser request to bypass Home approval. Viewing Developers or copying examples performs no settings mutation or restart.
+
The standalone development adapter reads http://127.0.0.1:24891 (configurable at build time with VITE_QORTIUM_NODE_API_URL). It permits GET/HEAD and does not advertise update/restart actions. The reference remains available when Core is offline. A developer browser fallback is not an authenticated admin route.
+
+
Status and peers
+
GET_NODE_STATUS returns a direct status object, with optional sync phase/percent, height/target/remaining blocks, minting possibility, connection counts and reachability fields. Missing fields stay unknown. FETCH_NODE_API returns an envelope with ok, HTTP status, contentType, body and parsed data. Node accepts direct objects from older adapters too; independent consumers should check response success and validate data before use.
+
{NODE_READ_PATHS.map(([path, meaning]) =>
{path}
{meaning}
)}
+
Chain peers and QDN/data peers are separate connections, with IP/I2P and inbound/outbound breakdowns. Node IDs can overlap between the two networks: a connection count is not a unique-node count. Diagnostic availability, backoff, connectability and an I2P session being up describe different conditions; none alone proves end-to-end QDN transfer. Peer addresses, IDs and settings may identify a device: inspect before sharing a diagnostic export.
+
Refresh loads a new snapshot. Optional reads can fail independently and fall back to empty/unknown displays; that is not proof the corresponding node data is empty. Node does not publish these reads to QDN. /admin/settings describes the current loaded settings object. Core reloads this object on save; its values do not prove restart-dependent services have adopted the change. Metadata can report differences between the file and that object.
+
+
Settings and units
+
GET_NODE_SETTINGS_METADATA is preferred; Node also attempts the read-only metadata path when unavailable. normalizeSettingsMetadata supports a plain writable map or JAXB writable.entry records. A top-level writable array is not supported by Node’s current normalizer. Missing or malformed writable metadata leaves fields read-only. Metadata includes per-field type/restartRequired, and may include pendingRestart, fileChanged, fileDiffersFromRuntime and fileComparisonError. Do not depend on a local settings-file path being exposed.
+
Current app allowlist ({PHASE_1_EDITABLE_SETTING_KEYS.length} fields, in the existing settings order):
+
{PHASE_1_EDITABLE_SETTING_KEYS.map(key =>
{key}
)}
+
Core adding a writable key does not automatically expose it in Node. Conversely, being in this allowlist does not make a field writable on every node. Keep editor defaults, validation and ordering consistent when intentionally adding a field.
+
+
Storage / retention
maxStorageCapacity displays decimal GB ({STORAGE_CAPACITY_GIGABYTE_BYTES.toLocaleString('en-US')} bytes/GB); chatMessageRetentionPeriod displays hours ({CHAT_RETENTION_HOUR_MS.toLocaleString('en-US')} ms/hour). The current editor accepts at least 1 GB or 1 hour, with up to three decimal places; conversion rounds to safe integer native units. Invalid input does not enter the patch.
+
Numeric / version fields
Ports are integers from 1 to 65535. Peer limits are positive integers except minOutboundPeers, which permits zero. The minimum version uses three numeric components, each from 0 to 32767. Core remains the final validator.
+
Enums
Storage: {STORAGE_POLICY_OPTIONS.join(', ')}. Auto update: {AUTO_UPDATE_MODE_OPTIONS.join(', ')}. Transport selections: {TRANSPORT_SELECTION_OPTIONS.map(x => x.label).join(', ')}. Transport order is not a distinct UI choice; legacy reversed arrays normalize to the combined option.
+
+
relayModeEnabled is intentionally absent: Qortium relays QDN whenever QDN is enabled. Retention policy, capacity, public/private data and push-on-publish remain separate controls. The reference does not add new settings permissions.
+
+
Save and restart
+
Discover {SETTINGS_BRIDGE_ACTIONS.map((action, i) => {i ? ', ' : ''}{action})} independently. Node sends only changed, valid fields as {'{ action: "UPDATE_NODE_SETTINGS", settings: patch }'}. Home 2 also accepts patch/payload aliases, validates the writable keys before prompting and shows the selected node with current/proposed values. A single request is limited to 64 settings, with key lengths up to 120 characters and bounded display values.
+
Approval belongs to Home and each request. The response exposes saved and key lists applied, updated, removed, restartRequired; Home 2 removes filesystem paths from the update result. Save is distinct from applying a restart-required value. Re-read both settings and metadata; file comparisons can fail independently.
+
Current Core retains a pendingRestart marker after a saved value is reverted to its original value. Check fileChanged and fileDiffersFromRuntime separately: these compare the file with the loaded settings object, not every service’s effective configuration. Neither a marker nor an empty file comparison alone proves whether a service needs restart. Node builds patches against its last settings snapshot; Refresh explicitly reloads that snapshot and clears the draft. Do not restart automatically.
+
There is no expectedRevision contract here. Do not silently retry writes after timeout/disconnection; the change may already have reached Core. Refresh and review the actual state. Denial keeps the draft available. A successful save or explicit Refresh resets Node’s draft; switching tabs and Back/Forward preserves it.
+
RESTART_NODE is a separate explicit request with its own approval. It acknowledges a restart request, not health or completion. Node displays “restart requested”; subsequent status/metadata reads establish whether the node returned and saved values became effective. Restarting can interrupt node activity; saving does not automatically request it.
+
Canonical reference route: qdn://APP/Node/Node?view=developers; developer/reference aliases normalize to developers. ?page=settings is preserved while viewing the reference. Returning to Node Status or Core Settings removes reference view/section parameters. Section links retain host parameters, repeated unknown parameters and fragments; Home Back/Forward restores the workspace.
+
+
Bridge examples
Examples illustrate the contract, using no live device data. Copying does not execute them. Keep settings changes and restart calls behind an explicit user decision.