Aggiunta stili CSS per Kiosk, struttura HTML per la Mappa e Riferimenti ai Sensori

• Creato un nuovo file CSS per gli stili del chiosco (kiosk) con variabili, stili per le schede (card) e animazioni.
• Aggiunto un file HTML per l'interfaccia della mappa utilizzando Mapbox, inclusi gli stili e il JavaScript per le funzionalità della mappa.
• Introdotto un file JSON per i riferimenti ai sensori, definendo percorsi ed elementi per i dati di temperatura, vento, onde, posizione, batteria, motore e sistema.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Giuseppe Raffa
2026-04-23 16:19:11 +02:00
parent 41f33ce181
commit bb8d267cd4
85 changed files with 4293 additions and 5083 deletions

View File

@@ -0,0 +1,27 @@
const router = require('express').Router();
const {
FORECAST_CURRENT,
FORECAST_HOURLY,
MARINE_CURRENT,
MARINE_HOURLY,
LOG_PATHS
} = require('../../rules');
const api_url = process.env.API_URL || 'http://api-services:3003';
router.get('/status', (req, res) => {
res.json({ cloud: 'active', api: api_url, version: '2.0' });
});
// Ritorna la configurazione statica corrente
router.get('/config', (req, res) => {
res.json({
forecast_current: FORECAST_CURRENT,
forecast_hourly: FORECAST_HOURLY,
marine_current: MARINE_CURRENT,
marine_hourly: MARINE_HOURLY,
log_paths: LOG_PATHS
});
});
module.exports = router;

View File

@@ -0,0 +1,16 @@
const router = require('express').Router();
const express = require('express');
const path = require('path')
const kioskPath = path.join(__dirname, '../../tools/kiosk');
router.use('/', express.static(kioskPath));
router.get('/', (req, res) => {
res.sendFile(path.join(kioskPath, 'dashboard.html'));
});
router.get('/api/', (req, res) => {});
module.exports = router;

View File

@@ -0,0 +1,38 @@
const router = require('express').Router();
const db = require('../../config/skFlow')
const config = require('../../config/configManager.js')
router.get('/', (req, res) => {
const { path } = req.query;
const data = db.get(path);
res.json(data);
});
router.get('/', (req, res) => {
const { source } = req.query;
const data = db.getBySource(source);
res.json(data);
});
router.get('/info', (req, res) => {
const info = {
telegram: config.getTelegramToken(),
sensor: {
name: config.getSensorName(),
code: config.getSensorCode()
},
other: {
api_url: process.env.API_URL,
realtime_url: process.env.REALTIME_URL,
realtime_socket_url: process.env.REALTIME_SOCKET_URL,
reconnect_delay: config.getReconnectDelay()
}
}
res.json(info);
});
module.exports = router;

View File

@@ -0,0 +1,34 @@
const router = require('express').Router();
const express = require('express');
const path = require('path');
const fs = require('fs');
const configManager = require('../../config/configManager.js');
const kioskPath = path.join(__dirname, '../../tools/kiosk');
const htmlFile = path.join(kioskPath, 'kiosk.html');
router.use('/', express.static(kioskPath));
router.get('/', (req, res) => {
const apiUrl = process.env.API_URL || 'https://api.mebboat.it';
const realtimeUrl = process.env.REALTIME_URL || 'https://realtime.mebboat.it';
const realtimeWsUrl = process.env.REALTIME_SOCKET_URL || 'wss://realtime.mebboat.it';
const sensorCode = configManager.getSensorCode();
const sensorName = configManager.getSensorName();
const esc = (s) => String(s || '').replace(/"/g, '&quot;');
const metas = `
<meta name="api-url" content="${esc(apiUrl)}">
<meta name="realtime-url" content="${esc(realtimeUrl)}">
<meta name="realtime-ws-url" content="${esc(realtimeWsUrl)}">
<meta name="sensor-code" content="${esc(sensorCode)}">
<meta name="sensor-name" content="${esc(sensorName)}">
`;
let html;
try { html = fs.readFileSync(htmlFile, 'utf8'); }
catch (e) { return res.status(500).send('kiosk.html not found'); }
html = html.replace('</head>', metas + '</head>');
res.set('Content-Type', 'text/html').send(html);
});
module.exports = router;

View File

@@ -0,0 +1,9 @@
const router = require('express').Router();
const path = require('path')
//Endpoints per controllare lo stato di un servizio di mappe da implementare poi..
router.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../tools/map/map.html'));
});
module.exports = router;

View File

@@ -0,0 +1,55 @@
const core = require('../../cores/logs.local')
const router = require('express').Router();
router.post('/start', async (req, res) => {
const { name } = req.body;
const session = await core.startRecording(name);
res.status(200).send({
status: 'Started',
session: session
});
});
router.get('/', (req, res) => {
const session = core.getSession();
if (session) {
res.status(200).send(session);
} else {
res.status(404).send({ session: 'Nessuna sessione attiva' });
}
});
router.get('/list', async (req, res) => {
const logs = await core.listLogs();
res.status(200).send(logs);
});
router.get('/:log', async (req, res) => {
const { log } = req.params
const data = await core.getLog(log);
if (data) {
res.status(200).send(data);
} else {
res.status(404).send({ error: 'Log non trovato' });
}
});
router.get('/download/:name', (req, res) => {
const name = req.params.name;
const filePath = core.getLogFile(name);
if (filePath) {
res.download(filePath);
} else {
res.status(404).send({ error: 'File non trovato' });
}
});
router.post('/stop', async (req, res) => {
await core.stopRecording();
res.status(200).send({
status: 'Stopped'
});
});
module.exports = router;

View File

@@ -1,56 +0,0 @@
/**
* Registers dataset recording control routes.
* @param {Object} router - Route wrapper with get/post methods
* @param {Object} app - SignalK app instance
*/
function registerDatasetRoutes(router, app) {
router.post("/dataset/start", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control not available" });
}
const result = app.datasetControl.start();
res.json({ success: result, message: result ? "Recording started" : "Already recording" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.post("/dataset/stop", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control not available" });
}
const result = app.datasetControl.stop();
res.json({ success: result, message: result ? "Recording stopped" : "No active recording" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.post("/dataset/restart", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control not available" });
}
const result = app.datasetControl.restart();
res.json({ success: result, message: "Recording restarted" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/dataset/status", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control not available" });
}
const status = app.datasetControl.getStatus();
res.json(status);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
}
module.exports = registerDatasetRoutes;

View File

@@ -1,63 +0,0 @@
const { getForecast, getSeaConditions } = require("../api_models/openmeteo.js");
/**
* Registers forecast-related routes.
* @param {Object} router - Route wrapper with get/post methods
* @param {Object} app - SignalK app instance
*/
function registerForecastRoutes(router, app) {
// Get current forecast data directly from OpenMeteo
router.get("/forecasts/data", async (req, res) => {
try {
const position = app.getSelfPath('navigation.position')?.value;
if (!position?.latitude || !position?.longitude) {
return res.status(503).json({ error: "Position not available" });
}
const mode = req.query.mode || 'both';
const [forecastData, wavesData] = await Promise.all([
getForecast(position, { mode }),
getSeaConditions(position, { mode })
]);
res.status(200).json({
forecast: forecastData,
sea: wavesData
});
} catch (error) {
console.error('[MEB] Error in /meb/forecasts/data:', error);
res.status(500).json({ error: error.message });
}
});
// Force update: fetch fresh hourly data from OpenMeteo
router.post("/forecasts/update", async (req, res) => {
try {
const position = app.getSelfPath('navigation.position')?.value;
if (!position?.latitude || !position?.longitude) {
return res.status(503).json({ error: "Position not available" });
}
const [forecastData, wavesData] = await Promise.all([
getForecast(position, { mode: 'both' }),
getSeaConditions(position, { mode: 'both' })
]);
if (!forecastData?.hourly || !wavesData?.hourly) {
return res.status(500).json({ error: "Hourly data not available from API" });
}
res.status(200).json({
forecast: forecastData,
sea: wavesData
});
} catch (error) {
console.error('[MEB] Error in /meb/forecasts/update:', error);
res.status(500).json({ error: error.message });
}
});
}
module.exports = registerForecastRoutes;

View File

@@ -1,30 +0,0 @@
const path = require("path");
const websPath = path.join(__dirname, "..", "public");
/**
* Registers helm/steering support routes.
* @param {Object} router - Route wrapper with get/post methods
*/
function registerHelmRoutes(router) {
router.get("/helm", (req, res) => {
try {
const side = req.query.side || "destra";
const helmPath = path.join(websPath, "steering_support", `helm_steering_${side}.html`);
res.status(200).sendFile(helmPath);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/helm/support", (req, res) => {
try {
const indexPath = path.join(websPath, "steering_support", "steering_support.html");
res.status(200).sendFile(indexPath);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}
module.exports = registerHelmRoutes;

View File

@@ -1,34 +0,0 @@
const registerMapRoutes = require("./map");
const registerHelmRoutes = require("./helm");
const registerDatasetRoutes = require("./dataset");
const registerForecastRoutes = require("./forecasts");
const registerTelegramRoutes = require("./telegram");
/**
* Registers all plugin routes under the /meb prefix.
* @param {Object} app - SignalK app instance
* @param {Object} settings - Plugin settings
*/
module.exports = function (app, settings) {
const router = {
get: (subPath, handler) => {
const fullPath = '/meb' + (subPath.startsWith('/') ? subPath : `/${subPath}`);
app.get(fullPath, handler);
},
post: (subPath, handler) => {
const fullPath = '/meb' + (subPath.startsWith('/') ? subPath : `/${subPath}`);
app.post(fullPath, handler);
}
};
// Health check
router.get("/ping", (req, res) => {
res.status(200).send("Ping is active!");
});
registerMapRoutes(router, app, settings);
registerHelmRoutes(router);
registerDatasetRoutes(router, app);
registerForecastRoutes(router, app);
registerTelegramRoutes(router);
};

18
plugin/routes/main.js Normal file
View File

@@ -0,0 +1,18 @@
// Il file generale che raggruppa le api
const router = require('express').Router();
const cloudRoutes = require('./collection/cloud')
const mapRoutes = require('./collection/map')
const dataRoutes = require('./collection/data')
const recRoutes = require('./collection/rec')
const dashboard = require('./collection/dashboard')
const kiosk = require('./collection/kiosk')
router.use('/cloud', cloudRoutes)
router.use('/map', mapRoutes)
router.use('/data', dataRoutes)
router.use('/rec', recRoutes)
router.use('/dashboard', dashboard)
router.use('/kiosk', kiosk)
module.exports = router

View File

@@ -1,43 +0,0 @@
const fs = require("fs");
const path = require("path");
const websPath = path.join(__dirname, "..", "public");
/**
* Registers map-related routes.
* @param {Object} router - Route wrapper with get/post methods
* @param {Object} app - SignalK app instance
* @param {Object} settings - Plugin settings
*/
function registerMapRoutes(router, app, settings) {
// Serve interactive map with Mapbox token injected
router.get('/map', (req, res) => {
const filePath = path.join(websPath, "map.html");
fs.readFile(filePath, "utf8", (err, html) => {
if (err) {
res.status(500).send("Error loading map");
return;
}
const token = settings?.mapboxKey ?? "";
const finalHtml = html.replace("{{MAPBOX_KEY}}", token);
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(finalHtml);
});
});
// Stream boat position and serve latest via API
let lastPosition = null;
app.streambundle.getSelfStream("navigation.position").onValue(pos => {
lastPosition = pos;
});
router.get('/map/boat', (req, res) => {
if (!lastPosition) {
return res.json({ error: "No position data available" });
}
res.json(lastPosition);
});
}
module.exports = registerMapRoutes;

View File

@@ -1,13 +0,0 @@
const { reloadBot } = require("../telegram/telegram.core.js");
module.exports = function (router) {
router.post("/telegram/reload", (req, res) => {
try {
reloadBot();
res.status(200).json({ status: "success", message: "Bot ricaricato." });
} catch (error) {
console.error("[MEB] Errore nel ricaricamento del bot da API:", error);
res.status(500).json({ status: "error", message: "Errore durante il reload del bot." });
}
});
};