update
This commit is contained in:
+185
-32
@@ -7,8 +7,8 @@ export function buildPointKey(isp, pointIndex) {
|
||||
export function sanitizeIspName(value) {
|
||||
const normalized = String(value || "").toLowerCase();
|
||||
|
||||
if (!/^isp\d+$/.test(normalized)) {
|
||||
throw new Error("Invalid ISP table name.");
|
||||
if (!/^(isp[a-z0-9_]*|trend_[a-z0-9_]+)$/.test(normalized)) {
|
||||
throw new Error("Invalid trend table name.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
@@ -45,6 +45,113 @@ export function parseTrendValue(rawValue, factor = 1) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildValueColumns() {
|
||||
const columns = [];
|
||||
|
||||
for (let index = 1; index <= 200; index += 1) {
|
||||
columns.push('`Wert' + index + '` TEXT NULL');
|
||||
}
|
||||
|
||||
return columns.join(", ");
|
||||
}
|
||||
|
||||
function hslToHex(hue, saturation, lightness) {
|
||||
const s = saturation / 100;
|
||||
const l = lightness / 100;
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
|
||||
const m = l - c / 2;
|
||||
|
||||
let red = 0;
|
||||
let green = 0;
|
||||
let blue = 0;
|
||||
|
||||
if (hue < 60) {
|
||||
red = c;
|
||||
green = x;
|
||||
} else if (hue < 120) {
|
||||
red = x;
|
||||
green = c;
|
||||
} else if (hue < 180) {
|
||||
green = c;
|
||||
blue = x;
|
||||
} else if (hue < 240) {
|
||||
green = x;
|
||||
blue = c;
|
||||
} else if (hue < 300) {
|
||||
red = x;
|
||||
blue = c;
|
||||
} else {
|
||||
red = c;
|
||||
blue = x;
|
||||
}
|
||||
|
||||
const toHex = (value) => Math.round((value + m) * 255).toString(16).padStart(2, "0");
|
||||
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
|
||||
}
|
||||
|
||||
function parseHslColor(value) {
|
||||
const match = String(value || "").match(/^hsl\((\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%\)$/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
hue: Number(match[1]),
|
||||
saturation: Number(match[2]),
|
||||
lightness: Number(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function createRandomPointColor(seed) {
|
||||
const hue = Math.round((seed * 47 + 17 + Math.random() * 28) % 360);
|
||||
const saturation = 72 + (seed % 4) * 4;
|
||||
const lightness = 45 + (seed % 3) * 4;
|
||||
return hslToHex(hue, saturation, lightness);
|
||||
}
|
||||
|
||||
export function normalizeEngineeringUnit(value) {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "°C";
|
||||
}
|
||||
|
||||
const normalized = raw
|
||||
.replace(/°/g, "°")
|
||||
.replace(/^\?C$/i, "°C")
|
||||
.replace(/^°\s*C$/i, "°C");
|
||||
|
||||
return normalized || "°C";
|
||||
}
|
||||
|
||||
function createDefaultPoint(index) {
|
||||
return {
|
||||
alias: `Wert${index}`,
|
||||
unit: "°C",
|
||||
factor: 1,
|
||||
kind: "analog",
|
||||
min: 0,
|
||||
max: 100,
|
||||
color: createRandomPointColor(index),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createWideTrendTable(pool, tableName) {
|
||||
const safeTable = sanitizeIspName(tableName);
|
||||
const sql = `
|
||||
CREATE TABLE IF NOT EXISTS \`${safeTable}\` (
|
||||
\`id\` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
\`datum\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
\`userlevel\` INT(1) NOT NULL,
|
||||
${buildValueColumns()},
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
await pool.query(sql);
|
||||
return safeTable;
|
||||
}
|
||||
|
||||
export async function getAvailableIsps(pool, dbName) {
|
||||
const [tables] = await pool.query(
|
||||
`
|
||||
@@ -59,43 +166,42 @@ export async function getAvailableIsps(pool, dbName) {
|
||||
) AS pointCount
|
||||
FROM information_schema.TABLES t
|
||||
WHERE t.TABLE_SCHEMA = ?
|
||||
AND t.TABLE_NAME LIKE 'isp%'
|
||||
AND (t.TABLE_NAME LIKE 'isp%' OR t.TABLE_NAME LIKE 'trend_%')
|
||||
ORDER BY t.TABLE_NAME
|
||||
`,
|
||||
[dbName]
|
||||
);
|
||||
|
||||
const latestMap = new Map();
|
||||
const stats = new Map();
|
||||
|
||||
await Promise.all(
|
||||
tables.map(async (table) => {
|
||||
const safeTable = sanitizeIspName(table.tableName);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT MAX(datum) AS latestTimestamp FROM \`${safeTable}\``
|
||||
`SELECT COUNT(*) AS rowCount, MAX(datum) AS latestTimestamp FROM \`${safeTable}\``
|
||||
);
|
||||
latestMap.set(safeTable, rows[0]?.latestTimestamp || null);
|
||||
stats.set(safeTable, rows[0] || { rowCount: 0, latestTimestamp: null });
|
||||
})
|
||||
);
|
||||
|
||||
return tables.map((table) => ({
|
||||
tableName: sanitizeIspName(table.tableName),
|
||||
pointCount: Number(table.pointCount || 0),
|
||||
latestTimestamp: latestMap.get(sanitizeIspName(table.tableName)) || null,
|
||||
}));
|
||||
return tables.map((table) => {
|
||||
const safeTable = sanitizeIspName(table.tableName);
|
||||
const info = stats.get(safeTable) || {};
|
||||
|
||||
return {
|
||||
tableName: safeTable,
|
||||
pointCount: Number(table.pointCount || 0),
|
||||
rowCount: Number(info.rowCount || 0),
|
||||
latestTimestamp: info.latestTimestamp || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createDefaultMetadata(isp, pointCount = 200) {
|
||||
const points = {};
|
||||
|
||||
for (let index = 1; index <= pointCount; index += 1) {
|
||||
points[String(index)] = {
|
||||
alias: `Wert${index}`,
|
||||
unit: "",
|
||||
factor: 1,
|
||||
kind: "analog",
|
||||
min: 0,
|
||||
max: 100,
|
||||
};
|
||||
points[String(index)] = createDefaultPoint(index);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -113,11 +219,62 @@ export async function ensureIspMetadata(redis, isp, pointCount = 200) {
|
||||
|
||||
if (existing) {
|
||||
const parsed = JSON.parse(existing);
|
||||
let changed = false;
|
||||
|
||||
if (!parsed.points) {
|
||||
parsed.points = {};
|
||||
changed = true;
|
||||
}
|
||||
|
||||
for (let index = 1; index <= pointCount; index += 1) {
|
||||
if (!parsed.points?.[String(index)]) {
|
||||
parsed.points[String(index)] = createDefaultMetadata(safeIsp, pointCount).points[String(index)];
|
||||
const pointKey = String(index);
|
||||
const defaultPoint = createDefaultPoint(index);
|
||||
|
||||
if (!parsed.points[pointKey]) {
|
||||
parsed.points[pointKey] = defaultPoint;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const point = parsed.points[pointKey];
|
||||
if (!point.alias) {
|
||||
point.alias = defaultPoint.alias;
|
||||
changed = true;
|
||||
}
|
||||
const normalizedUnit = normalizeEngineeringUnit(point.unit);
|
||||
if (normalizedUnit !== point.unit) {
|
||||
point.unit = normalizedUnit;
|
||||
changed = true;
|
||||
}
|
||||
if (!point.kind) {
|
||||
point.kind = defaultPoint.kind;
|
||||
changed = true;
|
||||
}
|
||||
if (!Number.isFinite(Number(point.factor))) {
|
||||
point.factor = defaultPoint.factor;
|
||||
changed = true;
|
||||
}
|
||||
if (!Number.isFinite(Number(point.min))) {
|
||||
point.min = defaultPoint.min;
|
||||
changed = true;
|
||||
}
|
||||
if (!Number.isFinite(Number(point.max))) {
|
||||
point.max = defaultPoint.max;
|
||||
changed = true;
|
||||
}
|
||||
const parsedHslColor = parseHslColor(point.color);
|
||||
if (parsedHslColor) {
|
||||
point.color = hslToHex(parsedHslColor.hue, parsedHslColor.saturation, parsedHslColor.lightness);
|
||||
changed = true;
|
||||
}
|
||||
if (!point.color) {
|
||||
point.color = defaultPoint.color;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await redis.set(key, JSON.stringify(parsed));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
@@ -162,11 +319,12 @@ export async function getPointDescriptors(redis, points) {
|
||||
key: buildPointKey(isp, pointIndex),
|
||||
displayName: metadata.displayName,
|
||||
alias: pointMeta.alias,
|
||||
unit: pointMeta.unit,
|
||||
unit: normalizeEngineeringUnit(pointMeta.unit),
|
||||
factor: pointMeta.factor,
|
||||
kind: pointMeta.kind,
|
||||
min: pointMeta.min,
|
||||
max: pointMeta.max,
|
||||
color: pointMeta.color || "",
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -218,9 +376,7 @@ export async function fetchTrendSeries(pool, redis, points, range) {
|
||||
|
||||
return {
|
||||
series: descriptors,
|
||||
rows: [...mergedRows.values()].sort((left, right) =>
|
||||
left.timestamp.localeCompare(right.timestamp)
|
||||
),
|
||||
rows: [...mergedRows.values()].sort((left, right) => left.timestamp.localeCompare(right.timestamp)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,10 +398,7 @@ export async function fetchLatestValues(pool, redis, points) {
|
||||
.map((descriptor) => `\`Wert${descriptor.pointIndex}\``)
|
||||
.join(", ");
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT datum, ${columns} FROM \`${isp}\` ORDER BY datum DESC LIMIT 1`
|
||||
);
|
||||
|
||||
const [rows] = await pool.query(`SELECT datum, ${columns} FROM \`${isp}\` ORDER BY datum DESC LIMIT 1`);
|
||||
const row = rows[0];
|
||||
|
||||
pointDescriptors.forEach((descriptor) => {
|
||||
@@ -267,9 +420,7 @@ export function normalizeRange(input = {}) {
|
||||
const maxRows = Number(input.maxRows || 4000);
|
||||
const now = new Date();
|
||||
const to = input.to ? new Date(input.to) : now;
|
||||
const from = input.from
|
||||
? new Date(input.from)
|
||||
: new Date(now.getTime() - presetDays * 24 * 60 * 60 * 1000);
|
||||
const from = input.from ? new Date(input.from) : new Date(now.getTime() - presetDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
return {
|
||||
from,
|
||||
@@ -277,3 +428,5 @@ export function normalizeRange(input = {}) {
|
||||
maxRows: Number.isFinite(maxRows) ? Math.max(100, Math.min(maxRows, 10000)) : 4000,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
+12
-3
@@ -1,4 +1,4 @@
|
||||
const SELECTION_SET_KEY = "selection:ids";
|
||||
const SELECTION_SET_KEY = "selection:ids";
|
||||
const SELECTION_SEQ_KEY = "selection:seq";
|
||||
|
||||
function safeParse(jsonValue) {
|
||||
@@ -54,12 +54,21 @@ export async function saveDashboard(redis, userId, dashboard) {
|
||||
}
|
||||
|
||||
export async function getPreferences(redis, userId) {
|
||||
return safeParse(await redis.get(`preferences:${userId}`)) || { theme: "system" };
|
||||
return safeParse(await redis.get(`preferences:${userId}`)) || {
|
||||
theme: "system",
|
||||
csvDelimiter: ";",
|
||||
sidebarPinned: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function savePreferences(redis, userId, preferences) {
|
||||
const current = await getPreferences(redis, userId);
|
||||
const payload = {
|
||||
theme: preferences.theme || "system",
|
||||
...current,
|
||||
...preferences,
|
||||
theme: preferences.theme || current.theme || "system",
|
||||
csvDelimiter: preferences.csvDelimiter || current.csvDelimiter || ";",
|
||||
sidebarPinned: typeof preferences.sidebarPinned === "boolean" ? preferences.sidebarPinned : current.sidebarPinned,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await redis.set(`preferences:${userId}`, JSON.stringify(payload));
|
||||
|
||||
Reference in New Issue
Block a user