Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
442bb6ccc9 | ||
|
|
ea20c55917 | ||
|
|
4ef7fa2d75 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.6.1",
|
"version": "2.6.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.6.1",
|
"version": "2.6.4",
|
||||||
"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.1",
|
"version": "2.6.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -572,6 +572,18 @@ body.drawer-open #scrim { opacity: 1; pointer-events: auto; }
|
|||||||
.dv-tile { position: relative; }
|
.dv-tile { position: relative; }
|
||||||
.dv-edit-btn { position: absolute; top: 5px; right: 5px; background: transparent; border: 1px solid var(--border); color: var(--muted); border-radius: 3px; font-size: 11px; line-height: 1; padding: 2px 5px; cursor: pointer; opacity: 0; }
|
.dv-edit-btn { position: absolute; top: 5px; right: 5px; background: transparent; border: 1px solid var(--border); color: var(--muted); border-radius: 3px; font-size: 11px; line-height: 1; padding: 2px 5px; cursor: pointer; opacity: 0; }
|
||||||
.dv-tile:hover .dv-edit-btn { opacity: 1; }
|
.dv-tile:hover .dv-edit-btn { opacity: 1; }
|
||||||
|
/* touch devices have no hover — keep the ✎ edit button always visible there */
|
||||||
|
@media (hover: none) { .dv-edit-btn { opacity: .85; } }
|
||||||
|
/* Little Blue service-tile edit affordance */
|
||||||
|
.lb-tile-wrap { position: relative; }
|
||||||
|
.lb-edit-btn { position: absolute; top: 5px; right: 5px; z-index: 5; background: var(--panel-2); border: 1px solid var(--border); color: var(--muted); border-radius: 3px; font-size: 11px; line-height: 1; padding: 2px 5px; cursor: pointer; opacity: 0; }
|
||||||
|
.lb-tile-wrap:hover .lb-edit-btn { opacity: 1; }
|
||||||
|
.lb-edit-btn:hover { color: var(--accent); border-color: var(--accent-dim); }
|
||||||
|
@media (hover: none) { .lb-edit-btn { opacity: .85; } }
|
||||||
|
.lb-edit { display: flex; flex-direction: column; gap: 4px; padding: 8px; }
|
||||||
|
.lb-edit .dv-edit-name, .lb-edit .dv-edit-grp { width: 100%; margin: 0; }
|
||||||
|
.lb-edit-btns { display: flex; gap: 4px; margin-top: 2px; }
|
||||||
|
.lb-edit-btns button { font-size: 11px; padding: 2px 8px; }
|
||||||
.dv-edit-btn:hover { color: var(--accent); border-color: var(--accent-dim); }
|
.dv-edit-btn:hover { color: var(--accent); border-color: var(--accent-dim); }
|
||||||
.dv-tile .dv-edit-name, .dv-tile .dv-edit-grp { margin: 2px 0; width: 100%; }
|
.dv-tile .dv-edit-name, .dv-tile .dv-edit-grp { margin: 2px 0; width: 100%; }
|
||||||
.dv-tile .dv-add, .dv-tile .dv-ignore, .dv-tile .ghost { margin-top: 4px; margin-right: 4px; font-size: 11px; padding: 2px 8px; }
|
.dv-tile .dv-add, .dv-tile .dv-ignore, .dv-tile .ghost { margin-top: 4px; margin-right: 4px; font-size: 11px; padding: 2px 8px; }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { serviceTile } from '../components/service_tile.js';
|
|||||||
import { isRemoteHost } from './service_url.js';
|
import { isRemoteHost } from './service_url.js';
|
||||||
|
|
||||||
const TITLE = { agents: 'Agents', infrastructure: 'Infrastructure', media: 'Media', other: 'Other' };
|
const TITLE = { agents: 'Agents', infrastructure: 'Infrastructure', media: 'Media', other: 'Other' };
|
||||||
|
const CATS = ['agents', 'infrastructure', 'media', 'other'];
|
||||||
let host, timer, scanning = false;
|
let host, timer, scanning = false;
|
||||||
|
|
||||||
async function promote(id) {
|
async function promote(id) {
|
||||||
@@ -17,6 +18,36 @@ function scan() {
|
|||||||
setTimeout(() => { scanning = false; load(); }, 30000); // LAN sweep ~25s
|
setTimeout(() => { scanning = false; load(); }, 30000); // LAN sweep ~25s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline edit form for a service (name / category / url / icon) — PATCH or DELETE.
|
||||||
|
function editForm(s) {
|
||||||
|
const nameI = el('input', { class: 'dv-edit-name', value: s.name || '', placeholder: 'name' });
|
||||||
|
const catS = el('select', { class: 'dv-edit-grp' }, ...CATS.map(c => el('option', { value: c }, TITLE[c])));
|
||||||
|
catS.value = s.category || 'other';
|
||||||
|
const urlI = el('input', { class: 'dv-edit-name', value: s.url || '', placeholder: 'http://host:port' });
|
||||||
|
const iconI = el('input', { class: 'dv-edit-name', value: s.icon || '', placeholder: 'icon slug e.g. plex' });
|
||||||
|
const save = el('button', { class: 'dv-add' }, 'Save');
|
||||||
|
save.onclick = async () => {
|
||||||
|
const patch = { name: nameI.value.trim(), category: catS.value, url: urlI.value.trim() };
|
||||||
|
const ic = iconI.value.trim(); if (ic) patch.icon = ic;
|
||||||
|
try { await api.patch('/api/health/services/' + s.id, patch); load(); } catch { /* */ }
|
||||||
|
};
|
||||||
|
const del = el('button', { class: 'ghost dv-ignore' }, 'Delete');
|
||||||
|
del.onclick = async () => { try { await api.del('/api/health/services/' + s.id); load(); } catch { /* */ } };
|
||||||
|
const cancel = el('button', { class: 'ghost' }, 'Cancel');
|
||||||
|
cancel.onclick = load;
|
||||||
|
return el('div', { class: 'tile lb-edit' }, nameI, catS, urlI, iconI,
|
||||||
|
el('div', { class: 'lb-edit-btns' }, save, del, cancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A service tile wrapped with an ✎ edit button that swaps to the edit form.
|
||||||
|
function tileWithEdit(s, remote) {
|
||||||
|
const wrap = el('div', { class: 'lb-tile-wrap' });
|
||||||
|
const edit = el('button', { class: 'lb-edit-btn', title: 'Edit service' }, '✎');
|
||||||
|
edit.onclick = (e) => { e.preventDefault(); e.stopPropagation(); mount(wrap, editForm(s)); };
|
||||||
|
mount(wrap, serviceTile(s, remote), edit);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
// Owner-only; returns a section element or null (skipped for non-owner / none).
|
// Owner-only; returns a section element or null (skipped for non-owner / none).
|
||||||
async function discoveredSection() {
|
async function discoveredSection() {
|
||||||
let cand;
|
let cand;
|
||||||
@@ -46,7 +77,7 @@ async function load() {
|
|||||||
el('span', { class: 'gname' }, TITLE[g.category] || g.category),
|
el('span', { class: 'gname' }, TITLE[g.category] || g.category),
|
||||||
el('span', { class: 'gcount' }, `${g.healthy}/${g.total} healthy`),
|
el('span', { class: 'gcount' }, `${g.healthy}/${g.total} healthy`),
|
||||||
el('span', { class: 'line' })),
|
el('span', { class: 'line' })),
|
||||||
el('div', { class: 'tiles' }, g.services.map(s => serviceTile(s, remote)))));
|
el('div', { class: 'tiles' }, g.services.map(s => tileWithEdit(s, remote)))));
|
||||||
const disc = await discoveredSection();
|
const disc = await discoveredSection();
|
||||||
mount(host,
|
mount(host,
|
||||||
el('div', { class: 'lbwrap' }, littleblueAvatar(),
|
el('div', { class: 'lbwrap' }, littleblueAvatar(),
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ import { seedFromConfig } from './lib/health/registry.js';
|
|||||||
import { mcpAuth } from './lib/api/middleware/mcp_auth.js';
|
import { mcpAuth } from './lib/api/middleware/mcp_auth.js';
|
||||||
import { handleMcp } from './lib/mcp/http.js';
|
import { handleMcp } from './lib/mcp/http.js';
|
||||||
import httpProxy from 'http-proxy';
|
import httpProxy from 'http-proxy';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
const VERSION = '2.6.1';
|
// Read the version from package.json so a deploy never serves a stale /health
|
||||||
|
// version (the old hardcoded const had to be bumped by hand and caused the
|
||||||
|
// health-gated deploy to roll back 3x when forgotten).
|
||||||
|
const VERSION = JSON.parse(readFileSync(new URL('./package.json', import.meta.url))).version;
|
||||||
|
|
||||||
// Proxy /terminal (+ its WebSocket) to ttyd on CT 300, so the embedded terminal
|
// Proxy /terminal (+ its WebSocket) to ttyd on CT 300, so the embedded terminal
|
||||||
// works whether the Void is reached via Traefik (void2-app.hynesy.com) OR the
|
// works whether the Void is reached via Traefik (void2-app.hynesy.com) OR the
|
||||||
|
|||||||
Reference in New Issue
Block a user