Display API: Self-Discoverable Visualization System
Overview
The Display API provides a plugin-based architecture for self-discoverable visualization. Instead of hardcoding display logic in web_user and tui, plugins can now register custom "display providers" that define how to extract and format data for visualization.
Key Benefits:
- No hardcoding - plugins define their own display sections
- Auto-discovery - web_user and tui automatically discover all registered providers
- Multiple formats - each provider can support JSON, HTML, text, table, or tree output
- Extensible - new plugins can register new display sections without modifying core code
- Unified API - single interface for both web and terminal UIs
Architecture
Core Components
-
DisplayProvider (abstract base class)
- Plugins subclass this to provide custom display sections
- Implements
extract_data()to fetch relevant information - Provides format methods:
format_json(),format_html(),format_text(),format_table(),format_tree()
-
DisplayRegistry (global singleton)
- Maintains a registry of all registered display providers
- Supports filtering by category and priority
- Thread-safe operations
-
web_user Integration
- New HTTP endpoints:
/api/display/providers,/api/display/render/{section},/api/display/all - Automatically calls display providers to build dashboards
- Supports multiple output formats via query parameters
- New HTTP endpoints:
-
tui Integration
- Dynamic section list that includes both built-in and provider sections
- New commands:
[m]for metadata,[s]for search - Seamlessly integrates provider output into BIOS console
Creating a Display Provider Plugin
Minimal Example
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
class MyCustomDisplayProvider(DisplayProvider):
"""Custom display provider for my plugin."""
def __init__(self):
super().__init__(
plugin_name='my_plugin',
section_id='custom_metrics',
display_name='Custom Metrics Dashboard'
)
self.category = 'custom'
self.priority = 75
self.refresh_interval_s = 5.0
def extract_data(self, node=None, **kwargs):
"""Extract data from node."""
return {
'total_items': 42,
'active': True,
'timestamp': int(__import__('time').time()),
}
def auto_load(config=None):
"""Plugin auto-load function."""
register_display_provider(MyCustomDisplayProvider())
return True
Full Example with Custom Formatting
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
class MetricsDisplayProvider(DisplayProvider):
def __init__(self):
super().__init__(
plugin_name='metrics_plugin',
section_id='system_metrics',
display_name='System Metrics'
)
self.category = 'metrics'
self.priority = 80
def extract_data(self, node=None, **kwargs):
# Custom data extraction logic
return {
'cpu_usage': 45.2,
'memory_usage': 78.5,
'network_io': 1024,
'disk_io': 512,
}
def format_html(self, data):
"""Custom HTML rendering."""
return f"""
<div style="padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: white;">
<h2>System Metrics</h2>
<table style="width: 100%; color: white;">
<tr><td>CPU</td><td>{data['cpu_usage']:.1f}%</td></tr>
<tr><td>Memory</td><td>{data['memory_usage']:.1f}%</td></tr>
<tr><td>Network I/O</td><td>{data['network_io']} bytes/s</td></tr>
<tr><td>Disk I/O</td><td>{data['disk_io']} bytes/s</td></tr>
</table>
</div>
"""
def format_text(self, data):
"""Custom text rendering for terminal."""
lines = [
"╔════════════════════════════════╗",
"║ System Metrics ║",
"╠════════════════════════════════╣",
f"║ CPU Usage: {data['cpu_usage']:>6.1f}% ║",
f"║ Memory Usage: {data['memory_usage']:>6.1f}% ║",
f"║ Network I/O: {data['network_io']:>6} bytes/s ║",
f"║ Disk I/O: {data['disk_io']:>6} bytes/s ║",
"╚════════════════════════════════╝"
]
return "\n".join(lines)
def auto_load(config=None):
register_display_provider(MetricsDisplayProvider())
return True
API Reference
DisplayProvider Class
Constructor
DisplayProvider(plugin_name: str, section_id: str, display_name: str = None)
Attributes:
plugin_name- Name of the plugin (e.g., 'id_allocator', 'test_plugin')section_id- Unique section identifier within the plugin (e.g., 'metrics', 'status')display_name- Human-readable display name (defaults to section_id)category- Category for grouping (default: 'plugin')priority- Display priority 0-100, higher = shown first (default: 50)refresh_interval_s- Suggested refresh interval (default: 2.0)
Abstract Method
extract_data(self, node=None, **kwargs) -> Dict[str, Any]
Must be implemented by subclasses. Returns a dict with extracted data.
Format Methods
All format methods receive the dict returned by extract_data() and return formatted output.
def format_json(self, data: Dict) -> Dict:
"""Return JSON-serializable dict. Default: returns data as-is."""
return data
def format_html(self, data: Dict) -> str:
"""Return HTML string. Default: generates basic table."""
return "<table>...</table>"
def format_text(self, data: Dict) -> str:
"""Return plain text. Default: pretty-prints JSON."""
return "key: value\n..."
def format_table(self, data: Dict) -> List[Dict]:
"""Return tabular data (rows). Default: wraps data in list."""
return [data]
def format_tree(self, data: Dict) -> Dict:
"""Return hierarchical structure. Default: returns data as-is."""
return data
def supports_format(self, fmt: DisplayFormat) -> bool:
"""Check if format is supported. Default: supports all."""
return True
Registry Functions
from opensynaptic.services.display_api import (
get_display_registry,
register_display_provider,
render_section,
collect_all_sections,
DisplayFormat,
)
# Register a provider
register_display_provider(MyProvider())
# Get the global registry
registry = get_display_registry()
# Get provider metadata
metadata = registry.get_metadata()
metadata = registry.get_metadata(plugin_name='my_plugin')
# List providers
providers = registry.list_all() # sorted by priority
providers = registry.list_by_category('metrics')
# List categories
categories = registry.list_categories()
# Render specific section
output = render_section(
plugin_name='my_plugin',
section_id='metrics',
fmt=DisplayFormat.JSON,
node=node_instance
)
# Collect all sections
all_data = collect_all_sections(fmt=DisplayFormat.JSON, node=node_instance)
web_user HTTP API
New Endpoints
GET /api/display/providers
Returns metadata about all registered display providers.
Response:
{
"ok": true,
"metadata": {
"total_providers": 3,
"categories": ["core", "metrics", "transport"],
"providers": [
{
"plugin_name": "id_allocator",
"section_id": "lease_metrics",
"display_name": "ID Lease Metrics",
"category": "metrics",
"priority": 80,
"refresh_interval_s": 5.0
}
]
}
}
GET /api/display/render/{plugin_name}:{section_id}?format=json
Render a specific display section.
Query Parameters:
format- Output format:json,html,text,table,tree(default:json)
Response:
{
"ok": true,
"section": "my_plugin:metrics",
"format": "json",
"data": { "metric1": 42, "metric2": 100 }
}
GET /api/display/all?format=json
Collect all registered display sections.
Query Parameters:
format- Output format (default:json)
Response:
{
"ok": true,
"format": "json",
"sections": {
"core": {
"node_stats": { "device_id": "sensor-001", "assigned_id": 12345 },
"pipeline_metrics": { "cache_entries": 256 }
},
"metrics": {
"system_metrics": { "cpu": 45.2, "memory": 78.5 }
}
},
"timestamp": 1711864234
}
tui Integration
Dynamic Section Discovery
tui now automatically includes display provider sections in its section list:
| OpenSynaptic BIOS Console (with Display API Providers)
| Built-in Sections:
| * 1. config
| 2. transport
| 3. pipeline
| 4. plugins
| 5. db
| 6. identity
| Display API Providers:
| 7. id_allocator:lease_metrics
| 8. test_plugin:performance
| 9. example_display:node_stats
New Commands
[1-N]- Switch to section by number (includes providers)[m]- Display metadata about all providers[s]- Search sections by partial name[a]- Show all sections (built-in + providers)[r]- Refresh current section[j]- Print current section as JSON[auto N]- Auto-refresh N cycles[i SEC]- Set refresh interval[p]- List available plugins[h]- Show help[q]- Quit
Example Session
bios> m
{
"builtin": ["config", "transport", "pipeline", "plugins", "db", "identity"],
"providers": {
"total_providers": 3,
"categories": ["metrics", "custom"],
"providers": [...]
}
}
bios> s metrics
{
"matching": [
"id_allocator:lease_metrics",
"test_plugin:performance"
]
}
bios> 7
[displays section: id_allocator:lease_metrics]
Example: Complete Plugin with Display Provider
File: plugins/my_analytics_plugin.py
"""
Analytics Plugin with Display Provider.
Provides custom metrics visualization via Display API.
"""
from typing import Dict, Any
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
import time
class AnalyticsMetricsDisplayProvider(DisplayProvider):
"""Display analytics metrics."""
def __init__(self):
super().__init__(
plugin_name='my_analytics',
section_id='metrics',
display_name='Analytics Metrics'
)
self.category = 'metrics'
self.priority = 85
self.refresh_interval_s = 5.0
def extract_data(self, node=None, **kwargs) -> Dict[str, Any]:
# Example: extract from node or any other source
return {
'requests_total': 15234,
'requests_per_second': 42.5,
'errors': 12,
'avg_latency_ms': 145.3,
'p99_latency_ms': 523.8,
'timestamp': int(time.time()),
}
def format_html(self, data: Dict[str, Any]) -> str:
return f"""
<div style="padding: 15px; background: #f0f4f8; border-radius: 8px;">
<h3>Analytics</h3>
<ul>
<li><strong>Total Requests:</strong> {data['requests_total']}</li>
<li><strong>RPS:</strong> {data['requests_per_second']}</li>
<li><strong>Errors:</strong> {data['errors']}</li>
<li><strong>Avg Latency:</strong> {data['avg_latency_ms']:.1f}ms</li>
<li><strong>P99 Latency:</strong> {data['p99_latency_ms']:.1f}ms</li>
</ul>
</div>
"""
class AnalyticsStatusDisplayProvider(DisplayProvider):
"""Display analytics system status."""
def __init__(self):
super().__init__(
plugin_name='my_analytics',
section_id='status',
display_name='Analytics Status'
)
self.category = 'custom'
self.priority = 70
def extract_data(self, node=None, **kwargs) -> Dict[str, Any]:
return {
'system_status': 'healthy',
'backend_connected': True,
'db_connected': True,
'cache_size_mb': 512,
}
def auto_load(config=None):
"""Register display providers when plugin loads."""
register_display_provider(AnalyticsMetricsDisplayProvider())
register_display_provider(AnalyticsStatusDisplayProvider())
from opensynaptic.utils import os_log
os_log.info(
'MY_ANALYTICS',
'DISPLAY_PROVIDERS_REGISTERED',
'Analytics display providers registered',
{'providers': ['metrics', 'status']}
)
return True
Best Practices
- Keep extract_data() fast - It's called frequently during refreshes
- Use appropriate categories - 'metrics', 'core', 'transport', 'custom', etc.
- Set reasonable priorities - Higher priority sections appear first
- Implement format methods selectively - Only override what you need
- Return JSON-serializable data - The JSON format is the default
- Handle missing node gracefully -
nodemay be None in some contexts - Use consistent display names - Make them user-friendly
- Test with both web_user and tui - Ensure output is suitable for both UIs
Testing Display Providers
Command Line
# List all providers
python -u src/main.py example_display list
# Render specific section
python -u src/main.py example_display render example_display node_stats --format json
# Collect all sections
python -u src/main.py example_display collect --format json
# Render as HTML
python -u src/main.py example_display render my_plugin metrics --format html
web_user
# View provider metadata
curl http://localhost:8765/api/display/providers
# Render specific section
curl http://localhost:8765/api/display/render/my_plugin:metrics?format=json
# Get all sections as HTML
curl http://localhost:8765/api/display/all?format=html
tui
# Enter TUI and press 'm' to view provider metadata
python -u src/main.py tui interactive --section identity
# Inside BIOS, use 's' to search for sections
# Use '7', '8', '9', etc. to view provider sections
Migration Guide: From Hardcoded to Display API
Before (Hardcoded in tui)
def _section_custom(self):
return {
'metric1': get_metric1(),
'metric2': get_metric2(),
}
_SECTION_METHODS = {
...
'custom': '_section_custom',
}
After (Display Provider)
# In your plugin
class MyDisplayProvider(DisplayProvider):
def __init__(self):
super().__init__('my_plugin', 'metrics')
def extract_data(self, node=None, **kwargs):
return {
'metric1': get_metric1(),
'metric2': get_metric2(),
}
def auto_load(config=None):
register_display_provider(MyDisplayProvider())
return True
Now tui and web_user automatically discover and render it without any hardcoding!
Troubleshooting
Provider not appearing in web_user or tui:
- Check plugin is being loaded (visible in
plugin-testor tui[p]command) - Verify
register_display_provider()is called inauto_load() - Check registry with curl:
GET /api/display/providers
Error rendering section:
- Check
extract_data()doesn't raise exceptions - Ensure returned data is JSON-serializable
- Check logs: search for
DISPLAY_RENDER_ERROR
HTML format not rendering properly:
- Test in
curl:GET /api/display/render/plugin:section?format=html - Verify HTML is properly escaped
- Check browser console for rendering errors
Performance issues:
- Reduce refresh_interval_s in provider
- Optimize extract_data() to be faster
- Consider caching data within the provider
See Also
src/opensynaptic/services/display_api.py- Core DisplayAPI implementationsrc/opensynaptic/services/example_display_plugin.py- Reference examplessrc/opensynaptic/services/tui/main.py- tui integrationsrc/opensynaptic/services/web_user/main.py- web_user integrationConfig.json-expose_sectionsconfiguration in web_user