-
Notifications
You must be signed in to change notification settings - Fork 0
Development Guide
This guide explains how to extend Epilykos with new visual blocks, data sources, and metrics. Intended for developers comfortable with JavaScript, Node.js, SQLite, and basic CSS/HTML.
- Node.js 18+
- Epilykos installed (Docker or local)
- JavaScript ES6+ (ES modules)
- Async/await patterns
- Basic SQL
| Module | Responsibility |
|---|---|
database.js |
SQLite connection, getConfig(), setConfig()
|
sessionAuth.js |
Session-based auth, CSRF, rate limiting |
ha.js |
Home Assistant polling |
mqtt.js |
MQTT broker connections |
modbus.js |
Modbus TCP/Serial |
external.js |
REST API polling |
bms.js |
Bluetooth BMS bridge |
history.js |
Legacy history snapshots |
grid.js |
Grid status, timeline |
solar.js |
Solar forecast (Solcast/Open-Meteo) |
savings.js |
PV savings |
metrics.js |
Current metrics, history |
metricsManager.js |
User-created metrics CRUD |
dashboard-config.js |
Layout config (JSON) |
backup.js |
Database backup/restore |
logger.js |
Winston logging |
utils.js |
Grid state parser |
All ES modules via <script type="module">.
| Module | Purpose |
|---|---|
main.js |
Entry — theme, config, WebSocket, polling |
dashboard.js |
CSS Grid + SortableJS, per-block config |
editor.js |
GridStack visual layout editor (/editor) |
updater.js |
Dispatches updateWithState(state) to blocks |
components/index.js |
componentBuilders registry (18 blocks) |
components/*.js |
Block builders + updaters |
api.js |
Centralised fetch |
charts.js |
Chart.js with multi-instance Maps |
forecast.js |
Multi-instance sparkline + weather |
tables.js |
Multi-instance data tables |
grid.js |
Grid card helpers |
utils/blockId.js |
Block ID generation |
utils/uid.js |
Scoped DOM ID helper |
Polling (30s) → buildDashboardState() → WebSocket broadcast
→ Frontend updateWithState(state)
→ Each block updater reads state.metrics
Fallback: polling /api/dashboard-state every 60s
-
12-column CSS Grid with
align-items: start(blocks keep independent heights) -
colSpan— grid-column span (1–12). Default 12. -
rowSpan— explicitheightin px (0 = auto). Default 0. -
bgColor— per-block background colour -
transparent— per-block transparency toggle - SortableJS drag-to-reorder (auth-only, lock/unlock toggle)
-
Multi-instance — all blocks support duplicates via
uid(base, blockId)scoped IDs
- GridStack visual layout designer
- Drag blocks from palette, reposition, resize
- Click ✕ to remove, + New / Delete / Rename dashboards
- Save & Exit persists layout
- Block positions saved as
colSpan/rowSpanin dashboard config
Create public/js/components/myBlock.js:
import { uid } from '../utils/uid.js';
export function buildMyBlock(block = {}) {
const id = block.id || '';
const config = block.config || {};
const metrics = config.metrics || {};
const myMetric = metrics.value || 'default_metric';
const container = document.createElement('div');
container.className = 'my-block card';
container.dataset.metricMap = JSON.stringify({ value: myMetric });
container.dataset.blockId = id;
container.innerHTML = `
<h3>${escapeHtml(config.title || 'My Block')}</h3>
<div class="my-value" data-metric="${escapeHtml(myMetric)}">--</div>
`;
return container;
}
function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }Pattern: Use uid(base, id) for all DOM IDs. Store metric config on dataset.metricMap. Use data-metric attributes. Provide defaults for backward compat.
export function updateMyBlock(state) {
document.querySelectorAll('.my-block').forEach(container => {
const id = container.dataset.blockId || '';
let mm; try { mm = JSON.parse(container.dataset.metricMap); } catch(e) { return; }
const m = state.metrics || {};
const val = m[mm.value]?.value;
if (val === undefined || val === null) return;
const el = container.querySelector('.my-value');
if (el) el.textContent = val.toFixed(1);
});
}Pattern: Iterate ALL containers of your type. Scope queries within each. Check for undefined/null values.
Edit components/index.js:
import { buildMyBlock, updateMyBlock } from './myBlock.js';
// Add to componentBuilders:
'my-block': buildMyBlock,Edit updater.js:
import { updateMyBlock } from './components/myBlock.js';
// Add to updateWithState:
if (activeLayout.some(b => b.type === 'my-block')) updateMyBlock(state);Edit settings.js. Add to the type dropdown:
<option value="my-block">My Block</option>Add config panel (in the config button handler):
} else if (block.type === 'my-block') {
const currentMetrics = block.config?.metrics || {};
configPanel.innerHTML = `
<label>Title</label><input type="text" class="config-title" value="${escapeHtml(block.config?.title||'')}" style="width:100%;margin-bottom:0.5rem;">
<label>Value Metric</label><select class="config-metric" data-role="value" style="width:100%;margin-bottom:0.5rem;">${generateMetricOptionsHtml(currentMetrics.value)}</select>
<button class="fetch-btn save-config">Save</button>`;
}Add save handler:
} else if (block.type === 'my-block') {
block.config.title = configPanel.querySelector('.config-title')?.value || '';
block.config.metrics = {};
configPanel.querySelectorAll('.config-metric').forEach(s => { const r = s.dataset.role; if (s.value) block.config.metrics[r] = s.value; });
}Add to style.css:
.my-value { font-size: var(--fs-large); font-weight: 600; color: var(--text); }The editor (editor.js) auto-discovers all builders from componentBuilders. Add a display name:
const names = { 'my-block': 'My Block', ... };import { uid } from '../utils/uid.js';
const id = block.id || '';
// In HTML: id="${uid('my-element', id)}"
// In updater: document.getElementById(uid('my-element', id))// Builder: store on container
container.dataset.metricMap = JSON.stringify(metrics);
// Updater: read from container
let mm; try { mm = JSON.parse(container.dataset.metricMap); } catch(e) { return; }
const val = state.metrics?.[mm.some_role]?.value;export function updateMyBlock(state) {
document.querySelectorAll('.my-block').forEach(container => {
// scope all queries within container
const id = container.dataset.blockId || '';
// ...
});
}Each block in the dashboard editor has:
- Type dropdown, Width dropdown, Height dropdown
- Background colour picker (
block.bgColor) - Transparency checkbox (
block.transparent) - Config button for per-block settings (metrics, title, etc.)
Create modules/mySource.js following the pattern in external.js. Register in server.js. Add the config key to database.js essentials. Add settings UI in settings.html.
- Use
constfor immutable values -
escapeHtml()for user input in innerHTML -
data-*attributes for metadata -
JSON.stringify/parsefor storing objects in data attributes - Check for
undefined/nullbefore displaying values - Polling functions never throw — log and continue
- Chart.js instances stored in Maps keyed by canvas ID
See gaugeCard.js (full-circle SVG gauge), multiValueCard.js (unlimited metrics), textCard.js (static HTML content), iframeCard.js (URL embed), or flowCardSquare.js (2×2 grid layout) for complete examples of different block patterns.