Backend services: - targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas, extract dispatch scoring weights, trim section dividers across 9 files - modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(), consolidate DM query factory, fix duplicate username fill bug, trim headers (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%) Frontend: - useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into 6 focused helpers (processOnlineStatus, processWanIPs, processRadios, processMeshNodes, processClients, checkRadioIssues) - EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments Documentation (17 → 13 files, -1,400 lines): - New consolidated README.md (architecture, services, dependencies, auth) - Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md - Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md - Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md - Update ROADMAP.md with current phase status - Delete CONTEXT.md (absorbed into README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
172 lines
6.0 KiB
JavaScript
172 lines
6.0 KiB
JavaScript
/**
|
|
* modem-bridge.js — Client for the modem-bridge Playwright service
|
|
*
|
|
* DEPENDENCY: modem-bridge container (port 3301 on Docker proxy network)
|
|
*
|
|
* Provides targo-hub with access to TP-Link ONU web GUI data
|
|
* by proxying requests to the modem-bridge headless Chromium service.
|
|
*
|
|
* ENDPOINTS EXPOSED (on targo-hub, port 3300):
|
|
* GET /modem/status?ip=X&user=Y&pass=Z — Full status (auto-login)
|
|
* GET /modem/dm/:oid?ip=X&user=Y&pass=Z — Raw DM read
|
|
* GET /modem/diagnostic?ip=X&user=Y&pass=Z — Full WiFi/mesh/WAN diagnostic
|
|
* GET /modem/screenshot?ip=X&user=Y&pass=Z — PNG screenshot
|
|
* GET /modem/sessions — List active sessions
|
|
* POST /modem/close — Close a session
|
|
*/
|
|
|
|
const http = require('http');
|
|
const { log, json: jsonResp, parseBody } = require('./helpers');
|
|
|
|
const BRIDGE_HOST = process.env.BRIDGE_HOST || 'modem-bridge';
|
|
const BRIDGE_PORT = parseInt(process.env.BRIDGE_PORT || '3301');
|
|
const BRIDGE_TOKEN = process.env.BRIDGE_TOKEN || '';
|
|
|
|
/**
|
|
* Make HTTP request to modem-bridge service
|
|
*/
|
|
function bridgeRequest(method, path, body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const opts = {
|
|
hostname: BRIDGE_HOST,
|
|
port: BRIDGE_PORT,
|
|
path,
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
};
|
|
if (BRIDGE_TOKEN) {
|
|
opts.headers['Authorization'] = `Bearer ${BRIDGE_TOKEN}`;
|
|
}
|
|
|
|
const bodyStr = body ? JSON.stringify(body) : '';
|
|
if (body) opts.headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
|
|
|
const req = http.request(opts, res => {
|
|
let data = '';
|
|
res.on('data', c => data += c);
|
|
res.on('end', () => {
|
|
try {
|
|
// Screenshots return binary
|
|
if (res.headers['content-type']?.includes('image/png')) {
|
|
resolve({ status: res.statusCode, body: Buffer.from(data, 'binary'), isPng: true });
|
|
} else {
|
|
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
}
|
|
} catch(e) {
|
|
resolve({ status: res.statusCode, body: data });
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) req.write(bodyStr);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ensure a session exists for the given modem, login if needed
|
|
*/
|
|
async function ensureSession(ip, user, pass, path) {
|
|
// Check if session already exists
|
|
const listRes = await bridgeRequest('GET', '/session/list');
|
|
if (listRes.status === 200 && Array.isArray(listRes.body)) {
|
|
const existing = listRes.body.find(s => s.ip === ip && s.loggedIn);
|
|
if (existing) return existing;
|
|
}
|
|
|
|
// Login
|
|
const loginRes = await bridgeRequest('POST', '/session/login', {
|
|
ip, user, pass, path: path || '/superadmin'
|
|
});
|
|
|
|
if (loginRes.status !== 200) {
|
|
throw new Error(`Modem login failed: ${loginRes.body?.error || 'Unknown error'}`);
|
|
}
|
|
|
|
return loginRes.body;
|
|
}
|
|
|
|
/**
|
|
* Route handler for targo-hub
|
|
*/
|
|
async function handleModemRequest(req, res, path) {
|
|
try {
|
|
// GET /modem/sessions — list active sessions
|
|
if (path === '/modem/sessions' && req.method === 'GET') {
|
|
const result = await bridgeRequest('GET', '/session/list');
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// POST /modem/close — close a session
|
|
if (path === '/modem/close' && req.method === 'POST') {
|
|
const body = await parseBody(req);
|
|
if (!body.ip) return jsonResp(res, 400, { error: 'Missing ip' });
|
|
const result = await bridgeRequest('DELETE', `/session/${body.ip}`);
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// GET /modem/health — bridge health (no auth needed)
|
|
if (path === '/modem/health' && req.method === 'GET') {
|
|
const result = await bridgeRequest('GET', '/health');
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// All other /modem/* routes need ip, user, pass
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const ip = url.searchParams.get('ip');
|
|
const user = url.searchParams.get('user') || 'superadmin';
|
|
const pass = url.searchParams.get('pass');
|
|
const loginPath = url.searchParams.get('path') || '/superadmin';
|
|
|
|
if (!ip || !pass) {
|
|
return jsonResp(res, 400, { error: 'Missing required params: ip, pass' });
|
|
}
|
|
|
|
// Ensure session exists
|
|
await ensureSession(ip, user, pass, loginPath);
|
|
|
|
// GET /modem/status — full status
|
|
if (path === '/modem/status' && req.method === 'GET') {
|
|
const result = await bridgeRequest('GET', `/modem/${ip}/status`);
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// GET /modem/dm/:oid — raw data manager read
|
|
const dmMatch = path.match(/^\/modem\/dm\/(.+)$/);
|
|
if (dmMatch && req.method === 'GET') {
|
|
const oid = dmMatch[1];
|
|
let queryStr = `?`;
|
|
if (url.searchParams.get('stack')) queryStr += `stack=${url.searchParams.get('stack')}&`;
|
|
if (url.searchParams.get('attrs')) queryStr += `attrs=${url.searchParams.get('attrs')}&`;
|
|
const result = await bridgeRequest('GET', `/modem/${ip}/dm/${oid}${queryStr}`);
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// GET /modem/diagnostic — full WiFi/mesh/WAN diagnostic
|
|
if (path === '/modem/diagnostic' && req.method === 'GET') {
|
|
const result = await bridgeRequest('GET', `/modem/${ip}/wifi/diagnostic`);
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
// GET /modem/screenshot — PNG
|
|
if (path === '/modem/screenshot' && req.method === 'GET') {
|
|
const result = await bridgeRequest('GET', `/modem/${ip}/screenshot`);
|
|
if (result.isPng) {
|
|
res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': result.body.length });
|
|
return res.end(result.body);
|
|
}
|
|
return jsonResp(res, result.status, result.body);
|
|
}
|
|
|
|
return jsonResp(res, 404, { error: 'Unknown modem endpoint' });
|
|
|
|
} catch(e) {
|
|
log('modem-bridge error:', e.message);
|
|
return jsonResp(res, 502, { error: e.message });
|
|
}
|
|
}
|
|
|
|
module.exports = { handleModemRequest };
|