const fs = require("fs"); const path = require("path"); const DEFAULT_BANDS = ["160m", "80m", "40m", "20m", "17m", "15m", "12m", "10m"]; async function createPlugin(ctx) { return { async execute(action) { if (action !== "getReport") { throw new Error(`Unknown action: ${action}`); } return readReport(ctx); }, async getStatus() { const report = readReport(ctx); return { overallStatus: report.overallStatus, generatedAt: report.generatedAt, bands: report.bands.length }; }, async health() { return { ok: true }; } }; } function readReport(ctx) { const defaultBase = "/var/www/html/stradnerkogel.arcg.at/vswr"; const htmlPath = String(ctx.getSetting("overviewHtmlPath", ctx.env.VSWR_OVERVIEW_HTML_PATH || `${defaultBase}/index.html`)).trim(); const metadataPath = String(ctx.getSetting("metadataPath", ctx.env.VSWR_METADATA_PATH || `${defaultBase}/metadata.txt`)).trim(); const imagesDirPath = String(ctx.getSetting("imagesDirPath", ctx.env.VSWR_IMAGES_DIR_PATH || `${defaultBase}/images`)).trim(); const publicImagesBaseUrl = String( ctx.getSetting("publicImagesBaseUrl", ctx.env.VSWR_IMAGES_BASE_URL || deriveImagesBaseUrl(ctx.env.SWR_OVERVIEW_URL || "")) ).trim(); const report = { source: "report-reader", generatedAt: null, overallStatus: "UNKNOWN", overviewUrl: String(ctx.env.SWR_OVERVIEW_URL || "") || null, bands: [] }; if (metadataPath && fs.existsSync(metadataPath)) { const raw = String(fs.readFileSync(metadataPath, "utf8") || "").trim(); if (raw) { report.overallStatus = raw.toUpperCase(); } try { report.generatedAt = fs.statSync(metadataPath).mtime.toISOString(); } catch { // ignore } } const bandMap = new Map(); for (const band of DEFAULT_BANDS) { bandMap.set(band, { band, status: "UNKNOWN", imageUrl: publicImagesBaseUrl ? `${publicImagesBaseUrl.replace(/\/$/, "")}/${band}.png` : null }); } if (htmlPath && fs.existsSync(htmlPath)) { const html = String(fs.readFileSync(htmlPath, "utf8") || ""); const overallMatch = /automatic\s+check\s+of\s+bands\s*\(([^)]+)\)\s*:\s*]*>\s*([^<]+)\s*<\/span>/i.exec(html); if (overallMatch) { report.generatedAt = normalizeDate(overallMatch[1]) || report.generatedAt; report.overallStatus = String(overallMatch[3] || overallMatch[2] || report.overallStatus).toUpperCase(); } const liRegex = /
  • \s*([0-9]{2,3}m)\s*-\s*]*>\s*([^<]+)\s*<\/span>/gi; let match; while ((match = liRegex.exec(html)) !== null) { const band = String(match[1] || "").toLowerCase(); const normalizedBand = DEFAULT_BANDS.find((entry) => entry.toLowerCase() === band) || match[1]; const status = String(match[3] || match[2] || "UNKNOWN").toUpperCase(); const prev = bandMap.get(normalizedBand) || { band: normalizedBand, status: "UNKNOWN", imageUrl: null }; bandMap.set(normalizedBand, { ...prev, status }); } const imgRegex = /]*src="([^"]+)"[^>]*alt="[^"]*?(\d{2,3}m)[^"]*"/gi; while ((match = imgRegex.exec(html)) !== null) { const src = String(match[1] || "").trim(); const band = String(match[2] || "").trim(); const prev = bandMap.get(band) || { band, status: "UNKNOWN", imageUrl: null }; bandMap.set(band, { ...prev, imageUrl: absolutizeImageUrl(src, publicImagesBaseUrl) }); } } if (imagesDirPath && fs.existsSync(imagesDirPath)) { for (const band of DEFAULT_BANDS) { const imgPath = path.join(imagesDirPath, `${band}.png`); if (!fs.existsSync(imgPath)) { continue; } const prev = bandMap.get(band) || { band, status: "UNKNOWN", imageUrl: null }; if (!prev.imageUrl && publicImagesBaseUrl) { prev.imageUrl = `${publicImagesBaseUrl.replace(/\/$/, "")}/${band}.png`; } try { const stat = fs.statSync(imgPath); prev.updatedAt = stat.mtime.toISOString(); } catch { // ignore } bandMap.set(band, prev); } } report.bands = Array.from(bandMap.values()); return report; } function deriveImagesBaseUrl(overviewUrl) { if (!overviewUrl) { return ""; } return `${String(overviewUrl).replace(/\/$/, "")}/images`; } function absolutizeImageUrl(src, baseUrl) { if (!src) return null; if (/^https?:\/\//i.test(src)) { return src; } const base = String(baseUrl || "").replace(/\/$/, ""); if (!base) { return src; } if (src.startsWith("/")) { return src; } if (src.startsWith("images/")) { return `${base}/${src.slice("images/".length)}`; } return `${base}/${src}`; } function normalizeDate(raw) { const value = String(raw || "").trim(); if (!value) return null; const parsed = new Date(value); if (!Number.isNaN(parsed.getTime())) { return parsed.toISOString(); } const normalized = value.replace(/\//g, "-"); const parsedNormalized = new Date(normalized); if (!Number.isNaN(parsedNormalized.getTime())) { return parsedNormalized.toISOString(); } return null; } module.exports = { createPlugin };