import assert from 'node:assert/strict'; import fs from 'node:fs'; import { createHash } from 'node:crypto'; const { pool } = await import('/app/backend/dist/db/index.js'); const { MikrotikClient } = await import('/app/backend/dist/services/mikrotik.js'); const { getAcmeSettingsPublic } = await import('/app/backend/dist/services/certificates-service.js'); const rows = (await pool.query("SELECT * FROM servers WHERE host IN ('194.226.121.150','104.165.244.7') ORDER BY host")).rows; assert.equal(rows.length, 2); assert(rows.every(s => s.enabled && s.port === 80 && !s.use_ssl)); const report = { date: '2026-10-14', block: '12.5.243', scope: 'Read-only availability, not container lifecycle or ACME issuance', hosts: [], requests: [], routerosWrites: 0, databaseWrites: 0, dnsWrites: 0, passwordChanges: 0 }; async function read(client, host, endpoint) { try { const result = await client.get(endpoint, 8000); report.requests.push({ host, path: endpoint, status: 200 }); return result; } catch (e) { report.requests.push({ host, path: endpoint, status: e.statusCode ?? null, errorName: e.name, errorBodySha256: e.body ? createHash('sha256').update(e.body).digest('hex') : null }); return undefined; } } try { for (const server of rows) { const client = new MikrotikClient({ host: server.host, port: 80, useSsl: false, verifySsl: false, username: server.username, password: server.password }); const packages = await read(client, server.host, '/system/package'); const resource = await read(client, server.host, '/system/resource'); const mode = await read(client, server.host, '/system/device-mode'); const disks = await read(client, server.host, '/disk'); const containers = await read(client, server.host, '/container'); const m = Array.isArray(mode) ? mode[0] : mode; report.hosts.push({ host: server.host, packages: Array.isArray(packages) ? packages.map(p => ({ name: p.name, version: p.version, disabled: p.disabled })) : null, resource: resource ? { version: resource.version, architecture: resource['architecture-name'], boardName: resource['board-name'], freeHddSpace: resource['free-hdd-space'], totalHddSpace: resource['total-hdd-space'], freeMemory: resource['free-memory'] } : null, deviceMode: m ? { mode: m.mode ?? null, container: m.container ?? null, flagged: m.flagged ?? null } : null, disks: Array.isArray(disks) ? disks.length : null, containers: Array.isArray(containers) ? { count: containers.length, statuses: containers.map(c => c.status ?? null) } : null }); } const acme = await getAcmeSettingsPublic(); report.acme = { tokenConfigured: acme.tokenConfigured, defaultZoneConfigured: Boolean(acme.defaultZoneId), directoryHost: null, directoryStatus: null }; const directory = new URL(acme.directoryUrl); assert(!directory.username && !directory.password); report.acme.directoryHost = directory.hostname; if (directory.protocol === 'https:' && ['acme-v02.api.letsencrypt.org','acme-staging-v02.api.letsencrypt.org'].includes(directory.hostname) && directory.pathname === '/directory') { const r = await fetch(directory, { signal: AbortSignal.timeout(8000) }); report.acme.directoryStatus = r.status; if (r.ok) { const data = await r.json(); report.acme.directoryKeys = Object.keys(data).sort(); } } } finally { await pool.end(); fs.writeFileSync('/checks/external-jobs243-availability.json', JSON.stringify(report, null, 2) + '\n'); console.log(JSON.stringify(report)); }