BREAKING EXPERIMENTAL: Holepunch-native hard migration (RPC core, shared storage, SDK v2)
BREAKING: All nodes must upgrade together. Legacy p2ns.core-request string messages are rejected; invite/consensus control plane uses protomux-rpc (invite.request, invite.ack, invite.relay*, consensus.*). Shared Corestore namespaces are the default for plugin DBs and drives; USE_SHARED_CORESTORE_NAMESPACES=false is debug-only. Admin plugin actions with params are validated before run. EXPERIMENTAL: End-to-end RPC invite path reuses existing handlers via adapters; invite wire still ships on the invite channel. Multi-peer invite/relay integration tests are not in CI yet (core-rpc-smoke only). SDK & channels: - channel-rpc.js, sdk.channels.rpc (register/request/event) - core-rpc.js for p2ns.core; action-params + plugin route validation - sdk.db.getCore/reopen, sdk.state.getPeerChannelSnapshot, sdk.metrics.getHolepunchStats (schema v1) Runtime: - p2ns.js: RPC-first core invite/consensus; Hyperswarm firewall/reconnect - channel-manager: required protomux-rpc per peer - db-shared-namespace-migration; drive-manager shared namespaces - proxy-server: invite.request RPC for joiners Plugins: file.drop, global.profile, peer.directory, domain.consensus, peer.visualize, example.plugin (RPC demo); peer.directory UI polish Also: CI/smoke scripts, diagnostics hardening, plugin config schema validation, admin action param modal, docs/RFCS, package-lock + engines.node >= 18
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
let pluginsData = [];
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Fetch plugins from API
|
||||
async function fetchPlugins() {
|
||||
try {
|
||||
@@ -449,6 +458,89 @@ function renderSettingInput(domain, key, setting) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeActionParamValue(param, rawValue) {
|
||||
if (param.type === 'number') {
|
||||
const num = Number(rawValue);
|
||||
if (Number.isNaN(num)) {
|
||||
throw new Error(`Parameter "${param.name}" must be a number`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
if (param.type === 'boolean') {
|
||||
return rawValue === true || rawValue === 'true' || rawValue === '1';
|
||||
}
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
async function collectActionParameters(action) {
|
||||
if (!action.params || action.params.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const formFields = action.params.map((param, index) => {
|
||||
const inputId = `plugin-action-param-${index}`;
|
||||
const type = param.type || 'string';
|
||||
const required = param.required ? 'required' : '';
|
||||
const placeholder = param.placeholder ? `placeholder="${escapeHtml(param.placeholder)}"` : '';
|
||||
const defaultValue = param.default !== undefined ? String(param.default) : '';
|
||||
const description = param.description
|
||||
? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(param.description)}</p>`
|
||||
: '';
|
||||
|
||||
if (type === 'boolean') {
|
||||
return `
|
||||
<label class="block text-sm theme-text-primary mb-3">
|
||||
<span class="block mb-1">${escapeHtml(param.label || param.name)}</span>
|
||||
<select id="${inputId}" class="w-full p-2 theme-input rounded">
|
||||
<option value="false" ${defaultValue === 'false' ? 'selected' : ''}>False</option>
|
||||
<option value="true" ${defaultValue === 'true' ? 'selected' : ''}>True</option>
|
||||
</select>
|
||||
${description}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<label class="block text-sm theme-text-primary mb-3">
|
||||
<span class="block mb-1">${escapeHtml(param.label || param.name)}${param.required ? ' *' : ''}</span>
|
||||
<input id="${inputId}" type="${type === 'number' ? 'number' : 'text'}"
|
||||
class="w-full p-2 theme-input rounded"
|
||||
value="${escapeHtml(defaultValue)}"
|
||||
${placeholder}
|
||||
${required} />
|
||||
${description}
|
||||
</label>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const confirmed = await window.ConfirmationModal.show({
|
||||
title: action.label || action.name || 'Run Action',
|
||||
message: `<div><p class="theme-text-secondary mb-3">${escapeHtml(action.description || 'Provide action parameters.')}</p><div>${formFields}</div></div>`,
|
||||
type: 'info',
|
||||
confirmText: 'Run Action',
|
||||
cancelText: 'Cancel',
|
||||
allowHTML: true,
|
||||
focusConfirm: false
|
||||
});
|
||||
|
||||
if (!confirmed) return null;
|
||||
|
||||
const params = {};
|
||||
for (let i = 0; i < action.params.length; i++) {
|
||||
const param = action.params[i];
|
||||
const element = document.getElementById(`plugin-action-param-${i}`);
|
||||
const rawValue = element ? element.value : '';
|
||||
if (param.required && String(rawValue).trim() === '') {
|
||||
throw new Error(`Parameter "${param.name}" is required`);
|
||||
}
|
||||
if (String(rawValue).trim() === '' && !param.required) {
|
||||
continue;
|
||||
}
|
||||
params[param.name] = normalizeActionParamValue(param, rawValue);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// Execute a plugin action
|
||||
async function executeAction(domain, actionName, action) {
|
||||
if (!action) {
|
||||
@@ -470,10 +562,13 @@ async function executeAction(domain, actionName, action) {
|
||||
}
|
||||
|
||||
// Collect parameters if any
|
||||
const params = {};
|
||||
if (action.params && action.params.length > 0) {
|
||||
// TODO: Show modal to collect parameters
|
||||
// For now, execute with empty params
|
||||
const params = await collectActionParameters(action);
|
||||
if (params === null) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.innerHTML = `${action.icon || '⚡'} ${action.label || actionName}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/plugins/${domain}/actions/${actionName}`, {
|
||||
|
||||
Reference in New Issue
Block a user