reorg
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
# Example Plugin
|
||||
|
||||
A comprehensive template plugin for P2NS. Use this as a starting point when creating new plugins.
|
||||
|
||||
## Overview
|
||||
|
||||
The Example Plugin demonstrates all the key features available to P2NS plugins:
|
||||
|
||||
- Basic HTTP request handling
|
||||
- Static file serving from `www/` directory
|
||||
- API endpoints with query parameters
|
||||
- Plugin lifecycle hooks (`onInit`, `onShutdown`)
|
||||
- Admin panel settings integration
|
||||
- Admin panel actions integration
|
||||
- Using settings in handlers and frontend
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### Admin Settings
|
||||
|
||||
The plugin registers four example settings that appear in the P2NS admin panel:
|
||||
|
||||
| Setting | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| `exampleString` | string | Greeting prefix used in API responses |
|
||||
| `exampleNumber` | number | Multiplier used in calculations (1-1000) |
|
||||
| `exampleBoolean` | boolean | Toggle to include timestamps in responses |
|
||||
| `exampleSelect` | select | Operation mode selector (Standard/Enhanced/Advanced) |
|
||||
|
||||
### Admin Actions
|
||||
|
||||
The plugin registers three actions callable from the admin panel:
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| **Test Action** 🧪 | Demonstrates using current settings in an action |
|
||||
| **Reset Data** 🔄 | Clears action history and cached data |
|
||||
| **Calculate** 🔢 | Performs calculation using the multiplier setting |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /api/hello`
|
||||
|
||||
Returns a greeting message using the configured greeting prefix.
|
||||
|
||||
**Query Parameters:**
|
||||
- `name` (optional): Name to greet (default: "World")
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Hello, World!",
|
||||
"timestamp": "2025-01-01T00:00:00.000Z" // Only if exampleBoolean is true
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/info`
|
||||
|
||||
Returns plugin information and current settings.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"plugin": "example.plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "A comprehensive template plugin...",
|
||||
"dnsReady": true,
|
||||
"connectedPeers": 5,
|
||||
"settings": {
|
||||
"exampleString": "Hello, World!",
|
||||
"exampleNumber": 42,
|
||||
"exampleBoolean": false,
|
||||
"exampleSelect": "option1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/calculate`
|
||||
|
||||
Performs a calculation using the multiplier setting.
|
||||
|
||||
**Query Parameters:**
|
||||
- `value` (optional): Number to multiply (default: 0)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input": 10,
|
||||
"multiplier": 42,
|
||||
"result": 420,
|
||||
"calculation": "10 × 42 = 420"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/mode`
|
||||
|
||||
Returns the current operation mode and available modes.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"mode": "option1",
|
||||
"description": "Standard mode - basic functionality",
|
||||
"availableModes": ["option1", "option2", "option3"]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/public-key`
|
||||
|
||||
Returns the local peer's public key.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"publicKey": "abc123...",
|
||||
"hasIdentity": true
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/actions`
|
||||
|
||||
Returns the action history and last action result.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"lastResult": { ... },
|
||||
"history": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/settings`
|
||||
|
||||
Returns all current plugin settings.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"settings": { ... },
|
||||
"timestamp": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
example.plugin/
|
||||
├── config.json # Plugin configuration
|
||||
├── index.js # Backend handler with API logic
|
||||
├── README.md # This file
|
||||
├── app.log # Plugin log file (auto-generated)
|
||||
└── www/ # Frontend files
|
||||
├── index.html # Main HTML page
|
||||
├── icon.svg # Plugin icon
|
||||
├── manifest.json
|
||||
└── css/
|
||||
└── style.css
|
||||
```
|
||||
|
||||
## Creating Your Own Plugin
|
||||
|
||||
1. **Copy this plugin directory** to `plugin-sites/your.domain/`
|
||||
|
||||
2. **Update `config.json`**:
|
||||
```json
|
||||
{
|
||||
"name": "Your Plugin",
|
||||
"version": "1.0.0",
|
||||
"domain": "your.domain",
|
||||
"enabled": true,
|
||||
"description": "Description of your plugin",
|
||||
"author": "Your Name",
|
||||
"www": "www"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Modify `index.js`**:
|
||||
- Update the handler to serve your API endpoints
|
||||
- Register your own settings in `onInit()`
|
||||
- Register your own actions in `onInit()`
|
||||
- Add cleanup logic in `onShutdown()` if needed
|
||||
|
||||
4. **Update `www/index.html`** with your frontend
|
||||
|
||||
5. **Restart P2NS** - your plugin will be automatically discovered
|
||||
|
||||
## Plugin SDK Usage
|
||||
|
||||
This plugin demonstrates key SDK features:
|
||||
|
||||
```javascript
|
||||
const sdk = require('../../includes/plugins/sdk');
|
||||
|
||||
// Parse requests
|
||||
const { path, query, method } = sdk.router.parseRequest(req);
|
||||
|
||||
// Send JSON responses
|
||||
return sdk.router.json(res, { data: 'value' });
|
||||
|
||||
// Check system state
|
||||
if (sdk.utils.isDNSReady()) { ... }
|
||||
const peers = sdk.state.connectedPeers;
|
||||
|
||||
// Register settings
|
||||
sdk.admin.registerSetting('key', {
|
||||
type: 'string',
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
default: 'default value'
|
||||
});
|
||||
|
||||
// Get settings
|
||||
const value = await sdk.admin.getSetting('key', 'default');
|
||||
|
||||
// Register actions
|
||||
sdk.admin.registerAction('actionName', async (params) => {
|
||||
return { success: true, message: 'Done!' };
|
||||
}, {
|
||||
label: 'Action Label',
|
||||
description: 'What this action does',
|
||||
icon: '🚀'
|
||||
});
|
||||
|
||||
// Logging
|
||||
sdk.log.info('my.plugin', 'Message');
|
||||
sdk.log.error('my.plugin', 'Error message');
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Plugin System Guide](../../docs/PLUGINS.md)
|
||||
- [Plugin SDK Reference](../../docs/PLUGIN_SDK.md)
|
||||
- [REST API Documentation](../../docs/RESTAPI.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Example Plugin",
|
||||
"version": "1.0.0",
|
||||
"domain": "example.plugin",
|
||||
"enabled": true,
|
||||
"description": "A bare bones template plugin for P2NS - use this as a starting point for new plugins",
|
||||
"author": "P2NS",
|
||||
"homepage": "https://github.com/p2ns/p2ns",
|
||||
"license": "MIT",
|
||||
"icon": "code",
|
||||
"dependencies": {},
|
||||
"www": "www"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Example Plugin
|
||||
*
|
||||
* This is a comprehensive template plugin for P2NS.
|
||||
* Use this as a starting point when creating new plugins.
|
||||
*
|
||||
* This plugin demonstrates:
|
||||
* - Basic request handling
|
||||
* - Static file serving from www/ directory
|
||||
* - Simple API endpoint
|
||||
* - Plugin lifecycle hooks
|
||||
* - Admin settings integration
|
||||
* - Admin actions integration
|
||||
* - Using settings in handler and frontend
|
||||
*/
|
||||
|
||||
const sdk = require('../../includes/plugins/sdk');
|
||||
|
||||
// Store action results for display
|
||||
let lastActionResult = null;
|
||||
let actionHistory = [];
|
||||
|
||||
/**
|
||||
* Plugin Handler
|
||||
*
|
||||
* Handles all HTTP requests for example.plugin
|
||||
*
|
||||
* @param {Object} req - Node.js HTTP request object
|
||||
* @param {Object} res - Node.js HTTP response object
|
||||
* @returns {Promise<boolean>}
|
||||
* - true: Request was handled by this plugin
|
||||
* - false: Request should fall back to static file serving
|
||||
*/
|
||||
async function handler(req, res) {
|
||||
try {
|
||||
// Parse request using SDK router
|
||||
// This extracts the path and query parameters from the URL
|
||||
const { path, query, method } = sdk.router.parseRequest(req);
|
||||
|
||||
// Handle root path - return false to serve index.html from www/ directory
|
||||
if (path === '' || path === '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Example: Handle a simple API endpoint that uses settings
|
||||
// GET /api/hello?name=World
|
||||
if (path === 'api/hello' && method === 'GET') {
|
||||
// Get the greeting prefix from settings
|
||||
const greetingPrefix = await sdk.admin.getSetting('exampleString', 'Hello');
|
||||
const name = query.name || 'World';
|
||||
|
||||
// Use the boolean setting to determine if we should include timestamp
|
||||
const includeTimestamp = await sdk.admin.getSetting('exampleBoolean', false);
|
||||
|
||||
const response = {
|
||||
message: `${greetingPrefix}, ${name}!`,
|
||||
};
|
||||
|
||||
if (includeTimestamp) {
|
||||
response.timestamp = new Date().toISOString();
|
||||
}
|
||||
|
||||
return sdk.router.json(res, response);
|
||||
}
|
||||
|
||||
// Example: Handle another API endpoint that uses settings
|
||||
// GET /api/info
|
||||
if (path === 'api/info' && method === 'GET') {
|
||||
// Get all settings to show how they're being used
|
||||
const exampleString = await sdk.admin.getSetting('exampleString', 'Hello, World!');
|
||||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||||
const exampleBoolean = await sdk.admin.getSetting('exampleBoolean', false);
|
||||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||||
|
||||
return sdk.router.json(res, {
|
||||
plugin: 'example.plugin',
|
||||
version: '1.0.0',
|
||||
description: 'A comprehensive template plugin demonstrating admin settings and actions',
|
||||
dnsReady: sdk.utils.isDNSReady(),
|
||||
connectedPeers: sdk.state.connectedPeers,
|
||||
settings: {
|
||||
exampleString,
|
||||
exampleNumber,
|
||||
exampleBoolean,
|
||||
exampleSelect
|
||||
},
|
||||
settingsInUse: {
|
||||
greetingPrefix: exampleString,
|
||||
multiplier: exampleNumber,
|
||||
showTimestamps: exampleBoolean,
|
||||
mode: exampleSelect
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Example: Get user's public key
|
||||
// GET /api/public-key
|
||||
if (path === 'api/public-key' && method === 'GET') {
|
||||
const publicKey = sdk.state.localPeerId;
|
||||
return sdk.router.json(res, {
|
||||
publicKey: publicKey || null,
|
||||
hasIdentity: !!publicKey
|
||||
});
|
||||
}
|
||||
|
||||
// Example: API endpoint that uses number setting
|
||||
// GET /api/calculate?value=10
|
||||
if (path === 'api/calculate' && method === 'GET') {
|
||||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||||
const inputValue = parseFloat(query.value) || 0;
|
||||
const result = inputValue * exampleNumber;
|
||||
|
||||
return sdk.router.json(res, {
|
||||
input: inputValue,
|
||||
multiplier: exampleNumber,
|
||||
result: result,
|
||||
calculation: `${inputValue} × ${exampleNumber} = ${result}`
|
||||
});
|
||||
}
|
||||
|
||||
// Example: API endpoint that uses select setting
|
||||
// GET /api/mode
|
||||
if (path === 'api/mode' && method === 'GET') {
|
||||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||||
|
||||
const modeDescriptions = {
|
||||
option1: 'Standard mode - basic functionality',
|
||||
option2: 'Enhanced mode - additional features',
|
||||
option3: 'Advanced mode - full feature set'
|
||||
};
|
||||
|
||||
return sdk.router.json(res, {
|
||||
mode: exampleSelect,
|
||||
description: modeDescriptions[exampleSelect] || 'Unknown mode',
|
||||
availableModes: ['option1', 'option2', 'option3']
|
||||
});
|
||||
}
|
||||
|
||||
// Example: Get action history
|
||||
// GET /api/actions
|
||||
if (path === 'api/actions' && method === 'GET') {
|
||||
return sdk.router.json(res, {
|
||||
lastResult: lastActionResult,
|
||||
history: actionHistory.slice(-10) // Last 10 actions
|
||||
});
|
||||
}
|
||||
|
||||
// Example: Get all current settings
|
||||
// GET /api/settings
|
||||
if (path === 'api/settings' && method === 'GET') {
|
||||
const allSettings = await sdk.admin.getAllSettings();
|
||||
return sdk.router.json(res, {
|
||||
settings: allSettings,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Return false for all other routes to allow static file serving
|
||||
// This means CSS, JS, images, etc. will be served from www/ directory
|
||||
return false;
|
||||
} catch (err) {
|
||||
// Catch any unexpected errors
|
||||
sdk.log.error('example.plugin', `Error handling request: ${err.message}`);
|
||||
return sdk.router.error(res, 'Internal Server Error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Initialization Hook
|
||||
*
|
||||
* Called automatically when the plugin is loaded by the P2NS system.
|
||||
* Use this to initialize resources, set up timers, pre-load data, etc.
|
||||
*/
|
||||
async function onInit() {
|
||||
sdk.log.info('example.plugin', 'Plugin initialized');
|
||||
|
||||
// Example: Check if DNS service is ready
|
||||
if (sdk.utils.isDNSReady()) {
|
||||
sdk.log.info('example.plugin', 'DNS service is ready');
|
||||
} else {
|
||||
sdk.log.warn('example.plugin', 'DNS service is not ready yet');
|
||||
}
|
||||
|
||||
// Register actions that can be called from the admin panel
|
||||
sdk.admin.registerAction('testAction', async (params) => {
|
||||
sdk.log.info('example.plugin', `Test action called with params: ${JSON.stringify(params)}`);
|
||||
|
||||
// Get current settings to use in action
|
||||
const exampleString = await sdk.admin.getSetting('exampleString', 'Hello, World!');
|
||||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||||
const exampleBoolean = await sdk.admin.getSetting('exampleBoolean', false);
|
||||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
message: 'Test action executed successfully!',
|
||||
timestamp: new Date().toISOString(),
|
||||
params: params || {},
|
||||
settingsUsed: {
|
||||
greeting: exampleString,
|
||||
multiplier: exampleNumber,
|
||||
featureEnabled: exampleBoolean,
|
||||
mode: exampleSelect
|
||||
},
|
||||
calculation: `Using multiplier ${exampleNumber}: ${(params?.value || 10) * exampleNumber}`
|
||||
};
|
||||
|
||||
// Store result for API access
|
||||
lastActionResult = result;
|
||||
actionHistory.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'testAction',
|
||||
params,
|
||||
result
|
||||
});
|
||||
|
||||
// Keep history limited to last 50 actions
|
||||
if (actionHistory.length > 50) {
|
||||
actionHistory = actionHistory.slice(-50);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {
|
||||
label: 'Test Action',
|
||||
description: 'A test action that uses current settings and demonstrates the action system',
|
||||
icon: '🧪'
|
||||
});
|
||||
|
||||
// Register a more complex action that demonstrates different capabilities
|
||||
sdk.admin.registerAction('resetData', async (params) => {
|
||||
sdk.log.info('example.plugin', 'Reset data action called');
|
||||
|
||||
// Clear action history
|
||||
actionHistory = [];
|
||||
lastActionResult = null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Action history and cached data have been reset',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}, {
|
||||
label: 'Reset Data',
|
||||
description: 'Clear action history and reset cached data',
|
||||
icon: '🔄'
|
||||
});
|
||||
|
||||
// Register a calculation action that uses the number setting
|
||||
sdk.admin.registerAction('calculate', async (params) => {
|
||||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||||
const inputValue = params?.value ? parseFloat(params.value) : 10;
|
||||
const result = inputValue * exampleNumber;
|
||||
|
||||
const calcResult = {
|
||||
success: true,
|
||||
message: `Calculation completed using multiplier ${exampleNumber}`,
|
||||
input: inputValue,
|
||||
multiplier: exampleNumber,
|
||||
result: result,
|
||||
calculation: `${inputValue} × ${exampleNumber} = ${result}`,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
actionHistory.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'calculate',
|
||||
params,
|
||||
result: calcResult
|
||||
});
|
||||
|
||||
if (actionHistory.length > 50) {
|
||||
actionHistory = actionHistory.slice(-50);
|
||||
}
|
||||
|
||||
return calcResult;
|
||||
}, {
|
||||
label: 'Calculate',
|
||||
description: 'Perform a calculation using the number setting as multiplier',
|
||||
icon: '🔢'
|
||||
});
|
||||
|
||||
// Register settings that will appear in the admin panel
|
||||
sdk.admin.registerSetting('exampleString', {
|
||||
type: 'string',
|
||||
label: 'Greeting Prefix',
|
||||
description: 'The greeting prefix used in the /api/hello endpoint (e.g., "Hello", "Hi", "Greetings")',
|
||||
default: 'Hello, World!'
|
||||
});
|
||||
|
||||
sdk.admin.registerSetting('exampleNumber', {
|
||||
type: 'number',
|
||||
label: 'Multiplier',
|
||||
description: 'A number used as a multiplier in calculations (used in /api/calculate and actions)',
|
||||
default: 42,
|
||||
min: 1,
|
||||
max: 1000
|
||||
});
|
||||
|
||||
sdk.admin.registerSetting('exampleBoolean', {
|
||||
type: 'boolean',
|
||||
label: 'Include Timestamps',
|
||||
description: 'When enabled, API responses will include timestamp fields',
|
||||
default: false
|
||||
});
|
||||
|
||||
sdk.admin.registerSetting('exampleSelect', {
|
||||
type: 'select',
|
||||
label: 'Operation Mode',
|
||||
description: 'Select the operational mode for the plugin',
|
||||
default: 'option1',
|
||||
options: [
|
||||
{ value: 'option1', label: 'Standard Mode' },
|
||||
{ value: 'option2', label: 'Enhanced Mode' },
|
||||
{ value: 'option3', label: 'Advanced Mode' }
|
||||
]
|
||||
});
|
||||
|
||||
// Log initial settings
|
||||
const initialSettings = await sdk.admin.getAllSettings();
|
||||
sdk.log.info('example.plugin', `Initial settings loaded: ${JSON.stringify(initialSettings)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Shutdown Hook
|
||||
*
|
||||
* Called automatically when the plugin is unloaded by the P2NS system.
|
||||
* Use this to clean up resources, close connections, stop timers, etc.
|
||||
*/
|
||||
async function onShutdown() {
|
||||
sdk.log.info('example.plugin', 'Plugin shutting down');
|
||||
// Add cleanup code here if needed
|
||||
}
|
||||
|
||||
// Export the plugin interface
|
||||
module.exports = {
|
||||
handler, // HTTP request handler - processes all incoming requests
|
||||
onInit, // Called when plugin loads
|
||||
onShutdown // Called when plugin unloads
|
||||
};
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
/* Example Plugin Stylesheet - Dark Glass Theme */
|
||||
|
||||
:root {
|
||||
/* Modern Color Palette */
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--primary-light: #818cf8;
|
||||
--secondary: #8b5cf6;
|
||||
--accent: #ec4899;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
|
||||
/* Background Colors - Darker for glass effect */
|
||||
--bg-primary: #0a0e1a;
|
||||
--bg-secondary: #0f1419;
|
||||
--bg-tertiary: #1a1f2e;
|
||||
--bg-glass: rgba(255, 255, 255, 0.03);
|
||||
--bg-glass-hover: rgba(255, 255, 255, 0.06);
|
||||
--bg-glass-strong: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-tertiary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
|
||||
/* Borders & Shadows - Enhanced for glass */
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--border-color-strong: rgba(255, 255, 255, 0.15);
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glow: 0 0 30px rgba(99, 102, 241, 0.4);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background: linear-gradient(135deg, #0a0e1a 0%, #0f1419 50%, #1a1f2e 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(30px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(200%);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
backdrop-filter: blur(30px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(200%);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 40px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(15, 20, 30, 0.6);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(15, 20, 30, 0.6);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 15px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
margin-top: 10px;
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.5);
|
||||
color: var(--text-primary);
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.2), 0 2px 4px -1px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(99, 102, 241, 0.5);
|
||||
border-color: rgba(99, 102, 241, 0.7);
|
||||
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3), 0 4px 6px -2px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
color: var(--text-primary);
|
||||
padding: 15px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: 10px;
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: var(--text-primary);
|
||||
padding: 15px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: 10px;
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#plugin-info ul {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#plugin-info li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#plugin-info li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(30px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(200%);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 20px 40px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Public Key Display Styles */
|
||||
.public-key-display {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.public-key-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.public-key-header span {
|
||||
font-weight: 600;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.5);
|
||||
color: var(--text-primary);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 2px 4px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: rgba(99, 102, 241, 0.5);
|
||||
border-color: rgba(99, 102, 241, 0.7);
|
||||
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
background: rgba(16, 185, 129, 0.3);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.copy-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.public-key-value {
|
||||
background: rgba(10, 14, 26, 0.8);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 2px solid rgba(99, 102, 241, 0.4);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.public-key-value code {
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
word-break: break-all;
|
||||
padding: 0;
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Settings Display Styles */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 4px solid var(--primary);
|
||||
padding: 15px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.setting-item strong {
|
||||
color: var(--primary-light);
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.setting-item code {
|
||||
background: rgba(15, 20, 30, 0.6);
|
||||
color: var(--text-primary);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
display: block;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.settings-note {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9em;
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
/* Button Group Styles */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Action Display Styles */
|
||||
.action-result {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.action-result h3 {
|
||||
color: var(--primary-light);
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.action-history {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-history h3 {
|
||||
color: var(--primary-light);
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px;
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-item strong {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.history-item small {
|
||||
color: var(--text-tertiary);
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* API Note Styles */
|
||||
.api-note {
|
||||
margin-top: 10px;
|
||||
padding: 8px;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
backdrop-filter: blur(25px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(200%);
|
||||
border-left: 3px solid var(--primary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85em;
|
||||
color: var(--text-primary);
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<path fill="#3b82f6" d="M278.5 215.6L23 471c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l255.5-255.5L504.9 505c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9L312.4 181.7c-9.4-9.4-24.6-9.4-33.9 0zM280 64c0-17.7-14.3-32-32-32s-32 14.3-32 32V240c0 17.7 14.3 32 32 32s32-14.3 32-32V64zM32 280c-17.7 0-32 14.3-32 32s14.3 32 32 32H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zm448 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h32c17.7 0 32-14.3 32-32s-14.3-32-32-32H480zM120 400c-17.7 0-32 14.3-32 32s14.3 32 32 32h272c17.7 0 32-14.3 32-32s-14.3-32-32-32H120z"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 634 B |
@@ -0,0 +1,360 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<title>Example Plugin - P2NS</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Example Plugin</h1>
|
||||
<p class="subtitle">A comprehensive template plugin demonstrating admin settings and actions</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="info">
|
||||
<h2>Plugin Information</h2>
|
||||
<p>This plugin demonstrates how to use admin settings and actions in a P2NS plugin. Configure settings in the admin panel to see them affect the plugin behavior.</p>
|
||||
<div id="plugin-info" class="loading">Loading plugin info...</div>
|
||||
</section>
|
||||
|
||||
<section class="settings">
|
||||
<h2>Current Settings</h2>
|
||||
<p>These settings are configured in the admin panel and affect plugin behavior:</p>
|
||||
<div id="settings-display" class="loading">Loading settings...</div>
|
||||
<p class="settings-note"><strong>Note:</strong> Change these settings in the admin panel at <code>p2ns.admin</code> → Plugins → example.plugin → Settings</p>
|
||||
</section>
|
||||
|
||||
<section class="identity">
|
||||
<h2>Your Public Key</h2>
|
||||
<p>Your persistent Hyperswarm public key that identifies you in the P2NS Network:</p>
|
||||
<div id="public-key-container" class="loading">Loading public key...</div>
|
||||
</section>
|
||||
|
||||
<section class="api-demo">
|
||||
<h2>API Demo (Using Settings)</h2>
|
||||
<p>These API endpoints demonstrate how settings affect responses:</p>
|
||||
<ul>
|
||||
<li><code>GET /api/hello?name=World</code> - Uses "Greeting Prefix" setting</li>
|
||||
<li><code>GET /api/calculate?value=10</code> - Uses "Multiplier" setting</li>
|
||||
<li><code>GET /api/mode</code> - Uses "Operation Mode" setting</li>
|
||||
<li><code>GET /api/info</code> - Shows all current settings</li>
|
||||
</ul>
|
||||
<div class="button-group">
|
||||
<button onclick="testHello()">Test Hello API</button>
|
||||
<button onclick="testCalculate()">Test Calculate API</button>
|
||||
<button onclick="testMode()">Test Mode API</button>
|
||||
</div>
|
||||
<div id="api-result" class="result"></div>
|
||||
</section>
|
||||
|
||||
<section class="actions">
|
||||
<h2>Admin Actions</h2>
|
||||
<p>Actions can be executed from the admin panel. Results are shown below:</p>
|
||||
<div id="actions-display" class="loading">Loading action results...</div>
|
||||
<p class="settings-note"><strong>Note:</strong> Execute actions in the admin panel at <code>p2ns.admin</code> → Plugins → example.plugin → Actions</p>
|
||||
</section>
|
||||
|
||||
<section class="structure">
|
||||
<h2>Plugin Structure</h2>
|
||||
<pre><code>plugin-sites/example.plugin/
|
||||
├── config.json # Plugin metadata
|
||||
├── index.js # Plugin handler (uses settings & actions)
|
||||
└── www/ # Document root for static files
|
||||
├── index.html # This file (displays settings)
|
||||
└── css/
|
||||
└── style.css # Stylesheet</code></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Use this plugin as a starting point for your own P2NS plugins! Configure settings in the admin panel to see them in action.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load plugin info on page load
|
||||
async function loadPluginInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/info');
|
||||
const data = await response.json();
|
||||
document.getElementById('plugin-info').innerHTML = `
|
||||
<ul>
|
||||
<li><strong>Plugin:</strong> ${data.plugin}</li>
|
||||
<li><strong>Version:</strong> ${data.version}</li>
|
||||
<li><strong>Description:</strong> ${data.description}</li>
|
||||
<li><strong>DNS Ready:</strong> ${data.dnsReady ? 'Yes' : 'No'}</li>
|
||||
<li><strong>Connected Peers:</strong> ${data.connectedPeers}</li>
|
||||
</ul>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('plugin-info').innerHTML =
|
||||
'<p class="error">Failed to load plugin info: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load and display current settings
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/settings');
|
||||
const data = await response.json();
|
||||
const settings = data.settings || {};
|
||||
|
||||
document.getElementById('settings-display').innerHTML = `
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<strong>Greeting Prefix:</strong>
|
||||
<code>${settings.exampleString || 'Hello, World!'}</code>
|
||||
<span class="setting-desc">Used in /api/hello endpoint</span>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<strong>Multiplier:</strong>
|
||||
<code>${settings.exampleNumber || 42}</code>
|
||||
<span class="setting-desc">Used in /api/calculate endpoint</span>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<strong>Include Timestamps:</strong>
|
||||
<code>${settings.exampleBoolean ? 'Enabled' : 'Disabled'}</code>
|
||||
<span class="setting-desc">Controls timestamp in API responses</span>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<strong>Operation Mode:</strong>
|
||||
<code>${settings.exampleSelect || 'option1'}</code>
|
||||
<span class="setting-desc">Used in /api/mode endpoint</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('settings-display').innerHTML =
|
||||
'<p class="error">Failed to load settings: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load action results
|
||||
async function loadActions() {
|
||||
try {
|
||||
const response = await fetch('/api/actions');
|
||||
const data = await response.json();
|
||||
|
||||
let html = '';
|
||||
|
||||
if (data.lastResult) {
|
||||
html += `
|
||||
<div class="action-result">
|
||||
<h3>Last Action Result</h3>
|
||||
<div class="success">
|
||||
<p><strong>Action:</strong> ${data.lastResult.params?.action || 'testAction'}</p>
|
||||
<p><strong>Message:</strong> ${data.lastResult.message}</p>
|
||||
<p><strong>Timestamp:</strong> ${data.lastResult.timestamp}</p>
|
||||
${data.lastResult.settingsUsed ? `
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>Settings Used</summary>
|
||||
<pre style="margin-top: 5px;">${JSON.stringify(data.lastResult.settingsUsed, null, 2)}</pre>
|
||||
</details>
|
||||
` : ''}
|
||||
${data.lastResult.calculation ? `
|
||||
<p><strong>Calculation:</strong> ${data.lastResult.calculation}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (data.history && data.history.length > 0) {
|
||||
html += `
|
||||
<div class="action-history">
|
||||
<h3>Action History (Last ${data.history.length})</h3>
|
||||
<div class="history-list">
|
||||
${data.history.map(item => `
|
||||
<div class="history-item">
|
||||
<strong>${item.action}</strong> at ${new Date(item.timestamp).toLocaleString()}
|
||||
${item.params && Object.keys(item.params).length > 0 ? `
|
||||
<br><small>Params: ${JSON.stringify(item.params)}</small>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html += '<p class="loading">No action history yet. Execute an action from the admin panel to see results here.</p>';
|
||||
}
|
||||
|
||||
document.getElementById('actions-display').innerHTML = html;
|
||||
} catch (err) {
|
||||
document.getElementById('actions-display').innerHTML =
|
||||
'<p class="error">Failed to load actions: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Test the hello API endpoint (uses greeting prefix setting)
|
||||
async function testHello() {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = '<p class="loading">Calling API...</p>';
|
||||
|
||||
try {
|
||||
const name = prompt('Enter your name (or leave blank for default):', 'World') || 'World';
|
||||
const response = await fetch(`/api/hello?name=${encodeURIComponent(name)}`);
|
||||
const data = await response.json();
|
||||
|
||||
let html = `
|
||||
<div class="success">
|
||||
<p><strong>Response:</strong> ${data.message}</p>
|
||||
`;
|
||||
|
||||
if (data.timestamp) {
|
||||
html += `<p><strong>Timestamp:</strong> ${data.timestamp}</p>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<p class="api-note"><em>Note: The greeting prefix comes from the "Greeting Prefix" setting in the admin panel.</em></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = '<p class="error">API call failed: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Test the calculate API endpoint (uses multiplier setting)
|
||||
async function testCalculate() {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = '<p class="loading">Calling API...</p>';
|
||||
|
||||
try {
|
||||
const value = prompt('Enter a number to multiply:', '10') || '10';
|
||||
const response = await fetch(`/api/calculate?value=${encodeURIComponent(value)}`);
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
<p><strong>Calculation:</strong> ${data.calculation}</p>
|
||||
<p><strong>Input:</strong> ${data.input}</p>
|
||||
<p><strong>Multiplier (from settings):</strong> ${data.multiplier}</p>
|
||||
<p><strong>Result:</strong> ${data.result}</p>
|
||||
<p class="api-note"><em>Note: The multiplier comes from the "Multiplier" setting in the admin panel.</em></p>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = '<p class="error">API call failed: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Test the mode API endpoint (uses select setting)
|
||||
async function testMode() {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = '<p class="loading">Calling API...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mode');
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
<p><strong>Current Mode:</strong> ${data.mode}</p>
|
||||
<p><strong>Description:</strong> ${data.description}</p>
|
||||
<p><strong>Available Modes:</strong> ${data.availableModes.join(', ')}</p>
|
||||
<p class="api-note"><em>Note: The mode comes from the "Operation Mode" setting in the admin panel.</em></p>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = '<p class="error">API call failed: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load public key
|
||||
async function loadPublicKey() {
|
||||
try {
|
||||
const response = await fetch('/api/public-key');
|
||||
const data = await response.json();
|
||||
const container = document.getElementById('public-key-container');
|
||||
|
||||
if (data.publicKey) {
|
||||
container.innerHTML = `
|
||||
<div class="public-key-display">
|
||||
<div class="public-key-header">
|
||||
<span>Public Key</span>
|
||||
<button onclick="copyPublicKey()" class="copy-button" title="Copy to clipboard">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div class="public-key-value">
|
||||
<code id="public-key-text">${data.publicKey}</code>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = '<p class="error">No public key found. Your identity will be generated when you start the P2NS system.</p>';
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('public-key-container').innerHTML =
|
||||
'<p class="error">Failed to load public key: ' + err.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Copy public key to clipboard
|
||||
async function copyPublicKey() {
|
||||
const publicKeyText = document.getElementById('public-key-text');
|
||||
if (!publicKeyText) return;
|
||||
|
||||
const publicKey = publicKeyText.textContent;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(publicKey);
|
||||
|
||||
// Visual feedback
|
||||
const button = event?.target?.closest('.copy-button');
|
||||
if (button) {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Copied!
|
||||
`;
|
||||
button.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
// Use ConfirmationModal if available, otherwise fallback to alert
|
||||
if (window.ConfirmationModal) {
|
||||
window.ConfirmationModal.alert(
|
||||
'Failed to copy public key to clipboard: ' + err.message,
|
||||
{
|
||||
title: 'Error',
|
||||
type: 'danger',
|
||||
confirmText: 'OK'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
alert('Failed to copy public key to clipboard: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all data when page loads
|
||||
loadPluginInfo();
|
||||
loadSettings();
|
||||
loadPublicKey();
|
||||
loadActions();
|
||||
|
||||
// Refresh settings and actions every 5 seconds to show updates
|
||||
setInterval(() => {
|
||||
loadSettings();
|
||||
loadActions();
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Example Plugin",
|
||||
"short_name": "Example",
|
||||
"description": "A bare bones template plugin for P2NS - use this as a starting point for new plugins",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["utilities", "developer"],
|
||||
"lang": "en"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user