diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index d4a6bb4..75b8d44 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -74,6 +74,9 @@ // so relying on something else having loaded the registry first works on the AI tab and fatals // on the dashboard. require_once __DIR__ . '/ai_profiles.php'; +// The mesh pane the chat card can switch to. Required here rather than by each page, so a page +// that mounts a chat gets the option without having to know the store exists. +require_once __DIR__ . '/node_chat.php'; // The conversation store, on its own, with no styling and no chat widget attached. // @@ -2400,7 +2403,21 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
- + + + + + + +
>
@@ -2529,6 +2549,8 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
+
+ + + + true, 'profile' => 'varaverk', // All three stated rather than derived. Large is at the viewport cap here, and leaving medium // to be worked out from the resting height would land it on that same cap — two settings diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index d428f5b..1b1332f 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -140,6 +140,7 @@ span.vv-arr-qbadge { display:inline-block; width:auto; } // which is a lot of vocabulary to carry to a search box. Scoped to the tab so "why are the lists // unavailable" resolves against what is on screen. vv_ai_chat_markup('vv-arr-ai', [ + 'mesh' => true, 'profile' => 'varaverk', 'compact' => true, 'title' => 'Assistant', diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php index 420cee6..c14c9c2 100644 --- a/Plugin/unraid/pages/auth.php +++ b/Plugin/unraid/pages/auth.php @@ -691,6 +691,7 @@ $tabs = ['proxies' => 'Proxies', 'users' => 'Users & Groups', // different objects that all end up deciding whether one person can open one URL, and the // question is nearly always "which of these is stopping me". vv_ai_chat_markup('vv-au-ai', [ + 'mesh' => true, 'profile' => 'varaverk', 'compact' => true, 'title' => 'Assistant', diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index d774a03..9bd5c2a 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -371,6 +371,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]); // to the assistant on its own — so landing on the strict profile here would refuse // ordinary questions to guard against a mistake the server already prevents. vv_ai_chat_markup('vv-mon-ai', [ + 'mesh' => true, 'profile' => 'chat', 'compact' => true, 'title' => 'Assistant', diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index 3b37c8c..b765699 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -132,110 +132,16 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
Loading…
- -
- - -
-
-

Mesh Chat

- - - - - -
- -
-
Loading…
-
- -
- -
-
- - - - -
-
-
- - - -
- Encrypted over Tailscale between mesh nodes only — but stored as plain text on every - machine that receives it. Delete removes it from this machine, not from theirs. -
-
-
-
'varaverk', 'compact' => true, + // This is the partners' page — the mesh is what you came here to read, and the assistant + // is the thing you switch to. Every other page has it the other way round. + 'mesh' => true, + 'meshDefault' => true, 'title' => 'Assistant', 'height' => '300px', 'tall' => '500px', @@ -1373,200 +1279,6 @@ function vvPtLoad() { _vvPtReload = vvPtLoad; // expose to top-level toggle fns -// ── Mesh chat ───────────────────────────────────────────────────────────────── -// Kept apart from vvPtLoad's 10s poll: that redraws the whole page, and a redraw mid-sentence -// would take the textarea with it. -let _vvNc = { ch: '', me: '', msgs: [], lastRead: 0, chans: [], hosts: {}, - fmt: { bold: false, italic: false, underline: false } }; - -// Formatting and display choices are per browser, not per message — picking a house style once -// and having it forgotten on reload is the same complaint the web-search tick had. -const _VV_NC_PREF = 'vvNcPrefs'; -function _vvNcLoadPrefs() { - let p = {}; - try { p = JSON.parse(localStorage.getItem(_VV_NC_PREF) || '{}') || {}; } catch (_) {} - for (const k of ['color', 'font', 'size']) { - const el = document.getElementById('vv-nc-' + k); - if (el && p[k] != null) el.value = p[k]; - } - for (const k of ['names', 'follow']) { - const el = document.getElementById('vv-nc-' + k); - if (el && p[k] != null) el.checked = !!p[k]; - } - _vvNc.fmt = Object.assign(_vvNc.fmt, p.fmt || {}); - _vvNcPaintFmt(); -} -function vvNcPref() { - const p = { fmt: _vvNc.fmt }; - for (const k of ['color', 'font', 'size']) p[k] = document.getElementById('vv-nc-' + k)?.value ?? ''; - for (const k of ['names', 'follow']) p[k] = !!document.getElementById('vv-nc-' + k)?.checked; - try { localStorage.setItem(_VV_NC_PREF, JSON.stringify(p)); } catch (_) {} - _vvNcPaintFmt(); -} -function vvNcTog(k) { _vvNc.fmt[k] = !_vvNc.fmt[k]; vvNcPref(); } - -// The preview is the only way to see what B/I/U plus a colour actually look like together -// before committing them to a message everyone else keeps a copy of. -function _vvNcPaintFmt() { - for (const k of ['bold', 'italic', 'underline']) { - document.getElementById('vv-nc-' + k)?.classList.toggle('active', !!_vvNc.fmt[k]); - } - const pv = document.getElementById('vv-nc-preview'); - if (!pv) return; - const s = _vvNcStyle({ style: { - color: document.getElementById('vv-nc-color')?.value || '', - font: document.getElementById('vv-nc-font')?.value || '', - size: document.getElementById('vv-nc-size')?.value || '', - bold: _vvNc.fmt.bold, italic: _vvNc.fmt.italic, underline: _vvNc.fmt.underline, - }}, false); - pv.setAttribute('style', 'margin-left:6px;' + s); -} - -function vvNcToggleFmt() { - const row = document.getElementById('vv-nc-fmt'); - const btn = document.getElementById('vv-nc-fmt-btn'); - const open = row.style.display !== 'none'; - row.style.display = open ? 'none' : 'flex'; - btn.textContent = open ? 'Aa ▸' : 'Aa ▾'; -} - -// Style string for a message. Closed set on both ends — the server validates the same values, -// so nothing here can emit a property the store did not agree to. -function _vvNcStyle(m, mine) { - const st = m.style || {}; - const sizes = { sm: '11px', lg: '14px' }; - let s = `color:${st.color || (mine ? '#7a9a7a' : '#9aa')};`; - if (fonts[st.font]) s += `font-family:${fonts[st.font]};`; - s += `font-size:${sizes[st.size] || '12px'};`; - if (st.bold) s += 'font-weight:700;'; - if (st.italic) s += 'font-style:italic;'; - if (st.underline) s += 'text-decoration:underline;'; - return s; -} - -// HOST2 or unRAID-Jayred36, depending on the tick. The mapping comes from master.conf via the -// API rather than being reconstructed here. -function _vvNcWho(id) { - const full = document.getElementById('vv-nc-names')?.checked; - return full ? (_vvNc.hosts[id] || id) : id.toUpperCase(); -} - -function vvNcChans() { - fetch('/plugins/varaverk/api/node_chat.php?_=' + Date.now()) - .then(r => r.json()) - .then(d => { - if (!d.ok) return; - _vvNc.me = d.me; _vvNc.chans = d.channels || []; _vvNc.hosts = d.hostnames || {}; - const sel = document.getElementById('vv-nc-ch'); - if (!sel) return; - const keep = _vvNc.ch || (_vvNc.chans[0] || {}).id || ''; - sel.innerHTML = _vvNc.chans.map(c => - ``).join(''); - if (!_vvNc.ch) vvNcSelect(keep); else vvNcLoad(); - }) - .catch(() => {}); -} - -function vvNcSelect(ch) { _vvNc.ch = ch; vvNcLoad(); } - -function vvNcLoad() { - if (!_vvNc.ch) return; - fetch('/plugins/varaverk/api/node_chat.php?channel=' + encodeURIComponent(_vvNc.ch) + '&_=' + Date.now()) - .then(r => r.json()) - .then(d => { if (d.ok) { _vvNc.msgs = d.messages || []; _vvNc.lastRead = d.last_read || 0; vvNcRender(); } }) - .catch(() => {}); -} - -function vvNcRender() { - const box = document.getElementById('vv-nc-log'); - if (!box) return; - const unreadOnly = document.getElementById('vv-nc-unread')?.checked; - let msgs = _vvNc.msgs; - if (unreadOnly) msgs = msgs.filter(m => (m.ts || 0) > _vvNc.lastRead && m.from !== _vvNc.me); - - if (!msgs.length) { - box.innerHTML = `
` - + (unreadOnly ? 'Nothing new.' : 'No messages yet — say hello.') + `
`; - return; - } - const fonts = { mono: 'monospace', sans: 'system-ui,sans-serif', serif: 'Georgia,serif' }; - box.innerHTML = msgs.map(m => { - const mine = m.from === _vvNc.me; - const fresh = (m.ts || 0) > _vvNc.lastRead && !mine; - return `
-
- ${vvEscHtml(_vvNcWho(m.from))} - ${new Date((m.ts || 0) * 1000).toLocaleString()} - × -
-
${vvEscHtml(m.text)}
-
`; - }).join(''); - // Only when asked. Someone reading back through a thread must not be yanked to the bottom by - // the 15s poll landing a new line. - if (document.getElementById('vv-nc-follow')?.checked) box.scrollTop = box.scrollHeight; - - // Marking read is what makes "unread only" mean anything next time, so it happens on view — - // but not while the filter is on, or the list would empty itself as you read it. - if (!unreadOnly && msgs.length) { - fetch('/plugins/varaverk/api/node_chat.php', { - method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({ csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', - action: 'read', channel: _vvNc.ch }) - }).catch(() => {}); - } -} - -function vvNcEmoji(sel) { - const t = document.getElementById('vv-nc-text'); - if (t && sel.value) { t.value += sel.value; t.focus(); } - sel.selectedIndex = 0; -} - -function vvNcSend() { - const t = document.getElementById('vv-nc-text'); - const text = (t.value || '').trim(); - if (!text) return; - const st = document.getElementById('vv-nc-status'); - st.textContent = 'sending…'; - fetch('/plugins/varaverk/api/node_chat.php', { - method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({ - csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', - action: 'send', channel: _vvNc.ch, text, - color: document.getElementById('vv-nc-color').value, - font: document.getElementById('vv-nc-font').value, - size: document.getElementById('vv-nc-size').value, - bold: _vvNc.fmt.bold ? '1' : '', - italic: _vvNc.fmt.italic ? '1' : '', - underline: _vvNc.fmt.underline ? '1' : '', - }) - }).then(r => r.json()).then(d => { - if (!d.ok) { st.textContent = d.error || 'failed'; return; } - t.value = ''; - // Queued is not failed. A partner that is asleep gets it on the next flush, and saying so is - // the difference between "it did not send" and "they are not awake". - st.textContent = d.queued ? `queued for ${d.queued} offline node(s)` : 'sent'; - setTimeout(() => { st.textContent = ''; }, 4000); - vvNcLoad(); - }).catch(e => { st.textContent = 'error'; }); -} - -function vvNcDel(id) { - fetch('/plugins/varaverk/api/node_chat.php', { - method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({ csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', - action: 'delete', channel: _vvNc.ch, id }) - }).then(() => vvNcLoad()).catch(() => {}); -} - -_vvNcLoadPrefs(); -vvNcChans(); -setInterval(vvNcChans, 15000); - vvPtLoad(); setInterval(vvPtLoad, 10000); diff --git a/Plugin/unraid/pages/rsync.php b/Plugin/unraid/pages/rsync.php index 7c7b205..f0dfeb3 100644 --- a/Plugin/unraid/pages/rsync.php +++ b/Plugin/unraid/pages/rsync.php @@ -350,6 +350,7 @@ require_once dirname(__DIR__) . '/include/ai_chat.php'; // Chat for the same reason as the Watchdog tab: this is a page you open with a question about // this installation, and the answer should come from its own docs and state with sources. vv_ai_chat_markup('vv-ry-ai', [ + 'mesh' => true, 'profile' => 'varaverk', 'compact' => true, 'title' => 'Assistant', diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index e49b8df..2a8902e 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -1101,6 +1101,7 @@ Still the same two servers, two households, the same media stack running itself. is not a number that exists until layout has run. -->
true, 'profile' => 'varaverk', 'compact' => true, 'scopeLabel' => 'Scheduler', diff --git a/Plugin/unraid/pages/watchdog.php b/Plugin/unraid/pages/watchdog.php index 2cb69f7..2f8c4cd 100644 --- a/Plugin/unraid/pages/watchdog.php +++ b/Plugin/unraid/pages/watchdog.php @@ -244,6 +244,7 @@ $_vv_wd_installed = vv_wd_installed_containers(); // own defaults apply and the expand control still works against them. The Scheduler is the only // placement whose size is a share of something and therefore has to be told. vv_ai_chat_markup('vv-wd-ai', [ + 'mesh' => true, 'profile' => 'varaverk', 'compact' => true, 'title' => 'Assistant',