gigafibre-fsm/services/targo-hub/lib/modem-bridge.js
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
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>
2026-04-13 08:39:58 -04:00

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 };