Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f655ed27 | ||
|
|
25ac261862 |
@@ -1,9 +1,18 @@
|
|||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import * as services from '../../db/repos/monitored_services.js';
|
import * as services from '../../db/repos/monitored_services.js';
|
||||||
|
import * as devices from '../../db/repos/lan_devices.js';
|
||||||
import { log } from '../../log.js';
|
import { log } from '../../log.js';
|
||||||
|
|
||||||
export const NAME = 'discover.lan';
|
export const NAME = 'discover.lan';
|
||||||
|
|
||||||
|
// Well-known homelab ports → likely service, so candidates get a real name.
|
||||||
|
const PORT_SVC = {
|
||||||
|
2424: 'Void', 5055: 'Overseerr', 6767: 'Bazarr', 7878: 'Radarr', 8006: 'Proxmox VE',
|
||||||
|
8096: 'Jellyfin', 8123: 'Home Assistant', 8265: 'Tdarr', 8384: 'Syncthing', 8989: 'Sonarr',
|
||||||
|
9000: 'Portainer', 9090: 'Cockpit', 9696: 'Prowlarr', 11434: 'Ollama', 19999: 'Netdata',
|
||||||
|
32400: 'Plex'
|
||||||
|
};
|
||||||
|
|
||||||
// Common homelab web/service ports to probe.
|
// Common homelab web/service ports to probe.
|
||||||
const PORTS = [80, 81, 443, 2424, 3000, 3001, 5000, 5055, 6767, 6875, 7878, 8000,
|
const PORTS = [80, 81, 443, 2424, 3000, 3001, 5000, 5055, 6767, 6875, 7878, 8000,
|
||||||
8006, 8080, 8081, 8096, 8123, 8265, 8384, 8443, 8989, 9000, 9090, 9696, 11434, 19999, 32400, 60072];
|
8006, 8080, 8081, 8096, 8123, 8265, 8384, 8443, 8989, 9000, 9090, 9696, 11434, 19999, 32400, 60072];
|
||||||
@@ -55,13 +64,18 @@ export async function handler(job) {
|
|||||||
// 1) TCP sweep → live host:ports
|
// 1) TCP sweep → live host:ports
|
||||||
const open = (await mapPool(targets, 120, async (t) => (await _tcp(t.host, t.port)) ? t : null)).filter(Boolean);
|
const open = (await mapPool(targets, 120, async (t) => (await _tcp(t.host, t.port)) ? t : null)).filter(Boolean);
|
||||||
|
|
||||||
// 2) HTTP-probe each, build + upsert discovered candidates (no-clobber in the repo)
|
// 2) HTTP-probe each, build + upsert discovered candidates (no-clobber in the repo).
|
||||||
|
// Cross-reference the Network Devices band so candidates are named by service+device.
|
||||||
|
const deviceByIp = Object.fromEntries(
|
||||||
|
(await devices.listKnown()).filter(d => d.ip).map(d => [d.ip, d.name]));
|
||||||
let added = 0;
|
let added = 0;
|
||||||
for (const { host, port } of open) {
|
for (const { host, port } of open) {
|
||||||
const scheme = HTTPS_PORTS.has(port) ? 'https' : 'http';
|
const scheme = HTTPS_PORTS.has(port) ? 'https' : 'http';
|
||||||
const url = `${scheme}://${host}:${port}`;
|
const url = `${scheme}://${host}:${port}`;
|
||||||
const probe = await _http(url);
|
const probe = await _http(url);
|
||||||
const name = (probe && probe.title) || `${host}:${port}`;
|
const dev = deviceByIp[host];
|
||||||
|
const svc = PORT_SVC[port] || (probe && probe.title) || null;
|
||||||
|
const name = svc ? (dev ? `${svc} · ${dev}` : svc) : (dev ? `${dev} :${port}` : `${host}:${port}`);
|
||||||
const id = `disc-${host.replace(/\./g, '-')}-${port}`;
|
const id = `disc-${host.replace(/\./g, '-')}-${port}`;
|
||||||
const check = scheme === 'https' ? { type: 'tcp' } : { type: 'http' };
|
const check = scheme === 'https' ? { type: 'tcp' } : { type: 'http' };
|
||||||
const r = await services.upsertDiscovered({ id, name, category: 'other', host, url, check });
|
const r = await services.upsertDiscovered({ id, name, category: 'other', host, url, check });
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.6.5",
|
"version": "2.7.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.6.5",
|
"version": "2.7.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.6.5",
|
"version": "2.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ ul.plain li:last-child { border-bottom: none; }
|
|||||||
/* reserved for a future agent-output phase — unused now:
|
/* reserved for a future agent-output phase — unused now:
|
||||||
--hue-dross: #ff4f2e; --hue-yerin: #c45a4a; --hue-orthos: #6fa86a; */
|
--hue-dross: #ff4f2e; --hue-yerin: #c45a4a; --hue-orthos: #6fa86a; */
|
||||||
}
|
}
|
||||||
#sv-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; }
|
#sv-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; grid-auto-rows: 8px; grid-auto-flow: row dense; }
|
||||||
.sv-card { grid-column: span 6; } /* fallback; svCard sets an inline span (1–12) */
|
.sv-card { grid-column: span 6; } /* fallback; svCard sets an inline span (1–12) */
|
||||||
@media (max-width: 700px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } }
|
@media (max-width: 700px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } }
|
||||||
.sv-ed-span { display: inline-flex; align-items: center; gap: 3px; }
|
.sv-ed-span { display: inline-flex; align-items: center; gap: 3px; }
|
||||||
|
|||||||
@@ -27,6 +27,24 @@ let layout = { card_order: [], hidden: [], sizes: {} };
|
|||||||
|
|
||||||
const grid = () => document.getElementById('sv-cards');
|
const grid = () => document.getElementById('sv-cards');
|
||||||
|
|
||||||
|
// ---- masonry packing: cards keep their column span (width) but pack vertically by
|
||||||
|
// content height (via grid-row span over small auto-rows), so mismatched heights no
|
||||||
|
// longer leave gaps / rigid rows. ResizeObserver re-packs as async cards fill in.
|
||||||
|
const ROW_UNIT = 8, GRID_GAP = 16;
|
||||||
|
function packCard(node) {
|
||||||
|
if (!node || !node.isConnected) return;
|
||||||
|
const h = node.getBoundingClientRect().height;
|
||||||
|
if (h) node.style.gridRowEnd = 'span ' + Math.max(1, Math.ceil((h + GRID_GAP) / (ROW_UNIT + GRID_GAP)));
|
||||||
|
}
|
||||||
|
const ro = typeof ResizeObserver !== 'undefined'
|
||||||
|
? new ResizeObserver(entries => entries.forEach(e => packCard(e.target))) : null;
|
||||||
|
let repackRaf;
|
||||||
|
function repackAll() {
|
||||||
|
cancelAnimationFrame(repackRaf);
|
||||||
|
repackRaf = requestAnimationFrame(() => grid()?.querySelectorAll('.sv-card').forEach(packCard));
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined') window.addEventListener('resize', repackAll);
|
||||||
|
|
||||||
async function saveLayout() {
|
async function saveLayout() {
|
||||||
try { await api.put('/api/dashboard/layout', layout); }
|
try { await api.put('/api/dashboard/layout', layout); }
|
||||||
catch (e) { console.error('save layout', e); }
|
catch (e) { console.error('save layout', e); }
|
||||||
@@ -72,6 +90,7 @@ function mountOne(def) {
|
|||||||
const { root, body } = svCard({ ...def, span });
|
const { root, body } = svCard({ ...def, span });
|
||||||
root.appendChild(editOverlay(def));
|
root.appendChild(editOverlay(def));
|
||||||
grid().appendChild(root);
|
grid().appendChild(root);
|
||||||
|
ro?.observe(root); packCard(root);
|
||||||
try { def.mount(body); def.start && def.start(); active.push(def); }
|
try { def.mount(body); def.start && def.start(); active.push(def); }
|
||||||
catch (e) { body.appendChild(el('span', { class: 'muted' }, 'card failed')); console.error(def.id, e); }
|
catch (e) { body.appendChild(el('span', { class: 'muted' }, 'card failed')); console.error(def.id, e); }
|
||||||
}
|
}
|
||||||
@@ -160,7 +179,7 @@ async function resetLayout() {
|
|||||||
export async function render(main) {
|
export async function render(main) {
|
||||||
mainEl = main;
|
mainEl = main;
|
||||||
const myGen = ++renderGen;
|
const myGen = ++renderGen;
|
||||||
active.forEach(c => c.stop && c.stop()); active = []; stopHealthBand(); stopDevicesBand();
|
active.forEach(c => c.stop && c.stop()); active = []; ro?.disconnect(); stopHealthBand(); stopDevicesBand();
|
||||||
editing = false;
|
editing = false;
|
||||||
mount(main,
|
mount(main,
|
||||||
el('h1', { class: 'view-h1' }, 'Sacred Valley'),
|
el('h1', { class: 'view-h1' }, 'Sacred Valley'),
|
||||||
|
|||||||
Reference in New Issue
Block a user