This commit is contained in:
Raven Scott
2025-12-17 20:05:50 -05:00
commit 742e27d3f7
276 changed files with 89838 additions and 0 deletions
+238
View File
@@ -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
+14
View File
@@ -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"
}
+340
View File
@@ -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);
}
+4
View File
@@ -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

+360
View File
@@ -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"
}