feat(littleblue): edit (✎) affordance for service tiles + name 8 discovered services
Service tiles now have an inline edit (name/category/url/icon, save/delete) like the Devices band — fixes 'can't edit after adding'. Touch-visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { serviceTile } from '../components/service_tile.js';
|
||||
import { isRemoteHost } from './service_url.js';
|
||||
|
||||
const TITLE = { agents: 'Agents', infrastructure: 'Infrastructure', media: 'Media', other: 'Other' };
|
||||
const CATS = ['agents', 'infrastructure', 'media', 'other'];
|
||||
let host, timer, scanning = false;
|
||||
|
||||
async function promote(id) {
|
||||
@@ -17,6 +18,36 @@ function scan() {
|
||||
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).
|
||||
async function discoveredSection() {
|
||||
let cand;
|
||||
@@ -46,7 +77,7 @@ async function load() {
|
||||
el('span', { class: 'gname' }, TITLE[g.category] || g.category),
|
||||
el('span', { class: 'gcount' }, `${g.healthy}/${g.total} healthy`),
|
||||
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();
|
||||
mount(host,
|
||||
el('div', { class: 'lbwrap' }, littleblueAvatar(),
|
||||
|
||||
Reference in New Issue
Block a user