From f914dd0eca61cb64ce8f427915cb14209cbd51be Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 29 Aug 2026 10:31:10 +1000 Subject: [PATCH] Restore bottom performance history graphs --- README.md | 2 + dashboard/dist/index.js | 94 ++++++++++++++++++++++++++++++-- dashboard/dist/style.css | 2 +- dashboard/manifest.json | 2 +- plugin.yaml | 2 +- tests/test_validation_harness.py | 10 ++++ 6 files changed, 105 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 25b7079..e36743e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Native-like Hermes dashboard plugin for local Ollama model management and chat. - Chat is the default view when the Ollama Models plugin opens - Chat uses a conversation rail, central message timeline, and dedicated model/runtime controls - Messages support safe Markdown-style emphasis, fenced code blocks, copy, and retry actions +- Runtime operations and historical performance graphs are restored below chat: CPU usage/load, system memory, GPU usage/VRAM, disk, swap, resident Ollama weights, and resident model count +- Performance samples are retained locally for the last 120 one-second telemetry snapshots - The catalog is loaded separately from live status with server-side search, filters, and pagination - Natural composer behavior: Enter sends; Shift+Enter creates a new line - Paste images directly into the composer and drag/drop images, PDFs, and text files diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index d0d9ecb..ab86b9f 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -9,6 +9,7 @@ var API = "/api/plugins/ollama-manager"; var CHAT_STORAGE_KEY = "hermes.ollama-manager.chat.v1"; var PLACEMENT_STORAGE_KEY = "hermes.ollama-manager.placement.v1"; + var PERFORMANCE_STORAGE_KEY = "hermes.ollama-manager.performance.v1"; function readSavedPlacements() { try { @@ -20,6 +21,16 @@ function savePlacements(value) { try { window.localStorage.setItem(PLACEMENT_STORAGE_KEY, JSON.stringify(value || {})); } catch (_) {} } + function readSavedPerformanceSamples() { + try { + var raw = window.localStorage.getItem(PERFORMANCE_STORAGE_KEY); + var value = raw ? JSON.parse(raw) : []; + return Array.isArray(value) ? value.filter(function (sample) { return sample && typeof sample.captured_at === "number"; }).slice(-120) : []; + } catch (_) { return []; } + } + function savePerformanceSamples(value) { + try { window.localStorage.setItem(PERFORMANCE_STORAGE_KEY, JSON.stringify((value || []).slice(-120))); } catch (_) {} + } function readSavedChat() { try { var raw = window.localStorage.getItem(CHAT_STORAGE_KEY); @@ -217,6 +228,58 @@ ); } + function graphNumber(sample, key) { + if (!sample || sample[key] === null || sample[key] === undefined) return null; + var value = Number(sample[key]); + return isFinite(value) ? value : null; + } + function PerformanceGraph(props) { + var samples = props.samples || [], values = samples.map(function (sample) { return graphNumber(sample, props.valueKey); }); + var max = Number(props.max || 0), observed = values.reduce(function (result, value) { return value == null ? result : Math.max(result, value); }, 0); + if (!max) max = observed > 0 ? observed * 1.15 : 1; + var width = 240, height = 84, pad = 5, points = [], latest = null; + values.forEach(function (value, index) { + if (value == null) return; + latest = value; + var x = pad + (values.length > 1 ? index * (width - pad * 2) / (values.length - 1) : (width / 2)); + var y = height - pad - Math.max(0, Math.min(1, value / max)) * (height - pad * 2); + points.push(x.toFixed(1) + "," + y.toFixed(1)); + }); + var line = points.join(" "), area = points.length ? pad + "," + (height - pad) + " " + line + " " + (width - pad) + "," + (height - pad) : ""; + var formatted = latest == null ? "No samples yet" : (props.format ? props.format(latest) : Number(latest).toFixed(1)); + return h("article", { className: "ollama-performance-graph" }, + h("div", { className: "ollama-performance-graph-heading" }, h("div", null, h("strong", null, props.title), h("small", null, props.subtitle)), h("b", null, formatted)), + h("svg", { className: "ollama-performance-svg", viewBox: "0 0 240 84", role: "img", "aria-label": props.title + " history" }, + h("line", { x1: pad, y1: height - pad, x2: width - pad, y2: height - pad, className: "ollama-graph-grid" }), + h("line", { x1: pad, y1: height / 2, x2: width - pad, y2: height / 2, className: "ollama-graph-grid" }), + h("line", { x1: pad, y1: pad, x2: width - pad, y2: pad, className: "ollama-graph-grid" }), + area && h("polygon", { points: area, className: "ollama-graph-area" }), + line && h("polyline", { points: line, className: "ollama-graph-line" }) + ), + h("div", { className: "ollama-performance-graph-scale" }, h("span", null, "0"), h("span", null, props.range || "dynamic")) + ); + } + function PerformanceGraphs(props) { + var samples = props.samples || []; + function pct(value) { return value == null ? "n/a" : Number(value).toFixed(1) + "%"; } + function gib(value) { return value == null ? "n/a" : Number(value).toFixed(2) + " GiB"; } + function count(value) { return value == null ? "n/a" : String(Math.round(value)); } + return h("section", { className: "ollama-performance-graphs" }, + h("div", { className: "ollama-performance-graphs-heading" }, h("div", null, h("h3", null, "Performance history"), h("p", null, "CPU, GPU, memory, storage, swap, and Ollama residency over the last ", samples.length, " samples.")), h("span", null, samples.length ? "Live · 1 second" : "Waiting for telemetry")), + h("div", { className: "ollama-performance-grid" }, + h(PerformanceGraph, { title: "CPU usage", subtitle: "Total processor utilization", valueKey: "cpu_usage_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "CPU load", subtitle: "1-minute load average", valueKey: "cpu_load_1m", range: "dynamic", format: function (value) { return Number(value).toFixed(2); }, samples: samples }), + h(PerformanceGraph, { title: "System memory", subtitle: "Used RAM", valueKey: "memory_used_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "GPU usage", subtitle: "Aggregate GPU utilization", valueKey: "gpu_usage_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "GPU VRAM", subtitle: "Used video memory", valueKey: "gpu_vram_used_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "Disk usage", subtitle: "Root filesystem", valueKey: "disk_used_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "Swap usage", subtitle: "Used swap memory", valueKey: "swap_used_percent", max: 100, range: "100%", format: pct, samples: samples }), + h(PerformanceGraph, { title: "Ollama model weights", subtitle: "Resident model bytes", valueKey: "ollama_model_gib", range: "dynamic", format: gib, samples: samples }), + h(PerformanceGraph, { title: "Resident models", subtitle: "Loaded Ollama model count", valueKey: "resident_model_count", range: "dynamic", format: count, samples: samples }) + ) + ); + } + function RuntimePanel(props) { var runtime = props.runtime || {}, total = Number(runtime.memory_total_bytes || 0), used = Number(runtime.memory_used_bytes || 0), pct = total ? Math.min(100, used * 100 / total) : 0; var gpu = runtime.gpu || {}, cpu = runtime.cpu || {}, disk = runtime.disk || {}, models = runtime.model_memory || [], loading = runtime.model_loading || [], cores = cpu.cores || [], gpus = gpu.gpus || []; @@ -323,7 +386,7 @@ var metricsState = React.useState(null), metrics = metricsState[0], setMetrics = metricsState[1]; var aggregateState = React.useState(null), aggregate = aggregateState[0], setAggregate = aggregateState[1]; var runtimeState = React.useState(null), runtime = runtimeState[0], setRuntime = runtimeState[1]; - var samplesState = React.useState([]), samples = samplesState[0], setSamples = samplesState[1]; + var samplesState = React.useState(readSavedPerformanceSamples()), samples = samplesState[0], setSamples = samplesState[1]; var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1]; var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1]; var validationState = React.useState(null), validationReports = validationState[0], setValidationReports = validationState[1]; @@ -411,8 +474,26 @@ refreshConversations(); setNotice({ ok: "Conversation deleted from shared storage." }); } - function pollRuntime() { fetchJSON(API + "/runtime").then(function (value) { setRuntime(value); setSamples(function (old) { return old.concat([{ used: Number(value.memory_used_bytes || 0), total: Number(value.memory_total_bytes || 0) }]).slice(-60); }); }).catch(function () {}); } - React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 5000); return function () { clearInterval(timer); }; }, []); + function pollRuntime() { + fetchJSON(API + "/runtime").then(function (value) { + var total = Number(value.memory_total_bytes || 0), used = Number(value.memory_used_bytes || 0), gpu = value.gpu || {}, gpus = gpu.gpus || [], gpuTotal = gpus.reduce(function (sum, item) { return sum + Number(item.total_bytes || 0); }, 0), gpuUsed = gpus.reduce(function (sum, item) { return sum + Number(item.used_bytes || 0); }, 0), swapTotal = Number(value.swap_total_bytes || 0), swapUsed = Number(value.swap_used_bytes || 0); + setRuntime(value); + setSamples(function (old) { return old.concat([{ + captured_at: Number(value.captured_at || Date.now() / 1000), + cpu_usage_percent: value.cpu && value.cpu.usage_percent, + cpu_load_1m: value.cpu && value.cpu.load_average && value.cpu.load_average[0], + memory_used_percent: total ? used * 100 / total : null, + gpu_usage_percent: gpu.utilization_percent, + gpu_vram_used_percent: gpuTotal ? gpuUsed * 100 / gpuTotal : null, + disk_used_percent: value.disk && value.disk.used_percent, + swap_used_percent: swapTotal ? swapUsed * 100 / swapTotal : null, + ollama_model_gib: Number(value.ollama_model_bytes || 0) / (1024 * 1024 * 1024), + resident_model_count: Array.isArray(value.model_memory) ? value.model_memory.length : 0 + }]).slice(-120); }); + }).catch(function () {}); + } + React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []); + React.useEffect(function () { savePerformanceSamples(samples); }, [samples]); React.useEffect(function () { savePlacements(placements); }, [placements]); function toggleIn(setter, name) { setter(function (old) { return old.indexOf(name) >= 0 ? old.filter(function (item) { return item !== name; }) : old.concat([name]); }); } @@ -549,8 +630,13 @@ h("div", { className: "ollama-controls-heading" }, h("h3", null, "Chat controls"), h("small", null, "Model, quality, and runtime")), h(ModelPoolPanel, { models: models, loadedModels: loadedModels, selectedModels: selectedModels, primaryModel: selectedModels[0] || "", validatorModels: selectedModels.slice(1), poolSelection: poolSelection, placements: placements, busy: busy, onTogglePool: togglePoolModel, onPlacementChange: setPlacement, onToggleChat: toggleChatModel, onPrimaryChange: setPrimaryModel, onLoad: loadModel, onUnload: unloadModels }), h("details", { className: "ollama-advanced-control" }, h("summary", null, "Chat storage"), h(StoragePanel, { storage: storage, onConfigure: configureStorage, onInstall: installPostgres, onRefresh: refreshStorage })), - h("details", { className: "ollama-advanced-control" }, h("summary", null, "Runtime telemetry"), h(RuntimePanel, { runtime: runtime, samples: samples })) + h("div", { className: "ollama-advanced-note" }, "Runtime cards and historical performance graphs are shown in Operations / Performance below.") ) + ), + h("section", { className: "ollama-operations-section" }, + h("div", { className: "ollama-operations-heading" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "OPERATIONS / PERFORMANCE"), h("h3", null, "Runtime telemetry and historical graphs"), h("p", null, "The complete operational view is preserved below the chat so it remains available without competing with the conversation.")), h("span", null, "CPU · GPU · RAM · disk · swap")), + h(RuntimePanel, { runtime: runtime, samples: samples }), + h(PerformanceGraphs, { samples: samples }) ) ); } diff --git a/dashboard/dist/style.css b/dashboard/dist/style.css index 5c05459..d408fa3 100644 --- a/dashboard/dist/style.css +++ b/dashboard/dist/style.css @@ -12,4 +12,4 @@ .ollama-catalog-controls{display:grid;grid-template-columns:repeat(3,minmax(130px,1fr));gap:8px;align-items:end;margin-top:0;padding:10px;border:1px solid rgba(164,211,199,.16);border-radius:10px;background:rgba(10,31,28,.55)}.ollama-catalog-controls label{display:flex;flex-direction:column;gap:5px;color:#a5bfba;font-size:10px;text-transform:uppercase;letter-spacing:.06em}.ollama-catalog-checkbox{display:flex!important;flex-direction:row!important;align-items:center;gap:8px;grid-column:1 / -1;padding:8px 4px;color:#b8ead9!important;text-transform:none!important;letter-spacing:normal!important;cursor:pointer}.ollama-catalog-checkbox input{width:15px;height:15px;margin:0;accent-color:#75d2b7}.ollama-catalog-checkbox span{font-size:11px}.ollama-catalog-memory-bypass{color:#ffd89a!important;background:rgba(142,90,25,.12);border-radius:7px}.ollama-catalog-select{min-width:145px;border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;text-transform:none;letter-spacing:normal} @media(max-width:1000px){.ollama-nav-row{display:grid;grid-template-columns:1fr}.ollama-toolbar-disk{justify-self:end}.ollama-browse-row{grid-template-columns:1fr}.ollama-catalog-controls{margin-top:0}} @media(max-width:760px){.ollama-tabs{grid-template-columns:repeat(2,minmax(0,1fr))}.ollama-nav-row{gap:8px}.ollama-toolbar-disk{justify-self:stretch;grid-template-columns:auto auto;min-width:0}.ollama-browse-row{gap:8px}.ollama-catalog-controls{grid-template-columns:1fr;align-items:stretch}.ollama-catalog-select{width:100%}}.ollama-harness-primary,.ollama-harness-validators{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.ollama-harness-primary{min-width:260px}.ollama-harness-primary label{display:flex;align-items:center;gap:8px;color:#a5bfba;font-size:11px}.ollama-harness-primary select{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:8px;font:inherit;font-size:11px;max-width:260px}.ollama-harness-validators{flex-basis:100%;padding-top:8px;border-top:1px solid rgba(164,211,199,.14)}.ollama-harness-validators>strong{color:#d5e8e2;font-size:11px}.ollama-harness-ready,.ollama-harness-warning{flex-basis:100%;font-size:10px}.ollama-harness-ready{color:#9af1c7}.ollama-harness-warning{color:#ffd89a}.ollama-validation-evidence{margin-top:10px;padding:10px 12px;border:1px solid rgba(141,210,193,.22);border-radius:8px;background:rgba(10,31,28,.5);color:#a5bfba;font-size:11px}.ollama-validation-evidence summary{cursor:pointer;color:#b8ead9;font-weight:700}.ollama-validation-report{margin-top:10px;padding-top:8px;border-top:1px solid rgba(164,211,199,.12)}.ollama-validation-report strong{color:#effcf8;font-size:11px}.ollama-validation-report p{margin:4px 0 0;white-space:pre-wrap;line-height:1.45}.ollama-storage-panel{margin-top:14px;padding:14px 16px;border:1px solid rgba(141,210,193,.22);border-radius:10px;background:rgba(10,31,28,.5)}.ollama-storage-copy{display:flex;flex-direction:column;gap:4px;margin-top:10px}.ollama-storage-copy strong{color:#effcf8;font-size:12px}.ollama-storage-copy small,.ollama-storage-note{color:#a5bfba;font-size:10px}.ollama-storage-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.ollama-storage-note{display:block;margin-top:10px} -.ollama-thinking-status{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:14px;padding:14px 16px;border:1px solid rgba(117,210,183,.42);border-radius:10px;background:linear-gradient(90deg,rgba(46,111,96,.34),rgba(24,64,57,.5));box-shadow:0 0 20px rgba(74,190,158,.08)}.ollama-thinking-copy{flex:1;min-width:200px}.ollama-thinking-details{flex-basis:100%;padding:12px;border-top:1px solid rgba(164,211,199,.17);color:#a5bfba}.ollama-thinking-detail-grid{display:grid;grid-template-columns:repeat(5,minmax(100px,1fr));gap:8px;margin-bottom:8px}.ollama-thinking-detail-grid span{display:flex;flex-direction:column;gap:3px;padding:8px;border-radius:7px;background:rgba(71,117,108,.11);font-size:10px;color:#8fb5ac}.ollama-thinking-detail-grid strong{color:#e4f4ef;font-size:11px;overflow-wrap:anywhere}.ollama-thinking-details small{font-size:10px;color:#819b96}.ollama-thinking-status .thinking-stop{color:#ffb8b8;border-color:rgba(255,110,110,.45)}.ollama-composer.drop-active{border-color:rgba(117,210,183,.8);background:linear-gradient(135deg,rgba(33,92,79,.52),rgba(23,52,48,.62));box-shadow:0 0 24px rgba(117,210,183,.16)}.ollama-drop-hint{padding:9px;border:1px dashed rgba(117,210,183,.7);border-radius:7px;text-align:center;color:#b8ead9;font-size:11px;background:rgba(117,210,183,.08)}.ollama-thinking-spinner{display:flex;align-items:center;gap:4px;min-width:28px}.ollama-thinking-spinner span{width:7px;height:7px;border-radius:50%;background:#75d2b7;animation:ollama-thinking-pulse 1.1s ease-in-out infinite}.ollama-thinking-spinner span:nth-child(2){animation-delay:.18s}.ollama-thinking-spinner span:nth-child(3){animation-delay:.36s}.ollama-thinking-copy{display:flex;flex-direction:column;gap:3px}.ollama-thinking-copy strong{color:#effcf8;font-size:13px}.ollama-thinking-copy span{color:#b9d8d0;font-size:12px}.ollama-thinking-copy small{color:#8fb5ac;font-size:10px}@keyframes ollama-thinking-pulse{0%,80%,100%{opacity:.35;transform:scale(.8)}40%{opacity:1;transform:scale(1.2)}}.ollama-chat-shell{display:grid;grid-template-columns:240px minmax(0,1fr) 330px;gap:14px;align-items:start;margin-top:14px}.ollama-conversation-rail,.ollama-chat-controls{min-width:0;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-conversation-rail{position:sticky;top:14px;max-height:calc(100vh - 110px);overflow:auto}.ollama-chat-controls{position:sticky;top:14px;max-height:calc(100vh - 110px);overflow:auto}.ollama-rail-heading,.ollama-controls-heading{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;margin-bottom:10px}.ollama-rail-heading h3,.ollama-controls-heading h3{margin:0;font-size:14px}.ollama-rail-heading small,.ollama-controls-heading small{color:#8fa9a4;font-size:10px}.ollama-conversation-rail .ollama-conversation-list{display:grid;gap:6px}.ollama-conversation-rail .ollama-conversation-list .ollama-button{display:flex;justify-content:space-between;gap:8px;width:100%;text-align:left}.ollama-conversation-rail .ollama-conversation-list .ollama-button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ollama-conversation-rail .ollama-conversation-list .ollama-button small{color:#8fa9a4;white-space:nowrap}.ollama-chat-main{display:flex;flex-direction:column;gap:12px;min-width:0}.ollama-chat-main .ollama-conversation{min-height:52vh;max-height:calc(100vh - 310px);overflow:auto;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(7,22,20,.56)}.ollama-chat-main .ollama-composer{position:sticky;bottom:10px;margin-top:0;padding:12px;border:1px solid rgba(141,210,193,.3);border-radius:12px;background:rgba(16,45,41,.96);box-shadow:0 10px 32px rgba(0,0,0,.24)}.ollama-chat-controls>.ollama-model-pool{margin-top:0;padding:0;border:0;background:none}.ollama-chat-controls .ollama-runtime-panel,.ollama-chat-controls .ollama-storage-panel{margin-top:10px;padding:0;border:0;background:none}.ollama-advanced-control{margin-top:12px;padding-top:10px;border-top:1px solid rgba(164,211,199,.14)}.ollama-advanced-control summary{cursor:pointer;color:#b8ead9;font-size:12px;font-weight:700}.ollama-message-meta{display:flex;justify-content:space-between;gap:10px;margin-bottom:6px;color:#8fa9a4;font-size:10px}.ollama-rich-text{line-height:1.55;overflow-wrap:anywhere}.ollama-rich-text>div{min-height:1.2em}.ollama-inline-code{padding:2px 5px;border-radius:4px;background:rgba(0,0,0,.25);color:#c9f4e4;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}.ollama-code-block{overflow:auto;margin:9px 0;padding:11px;border-radius:7px;background:#081714;color:#d7eee7;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.ollama-message-actions{display:flex;gap:6px;margin-top:9px;opacity:.75}.ollama-message-actions button{border:0;background:none;color:#8fb5ac;padding:2px 0;cursor:pointer;font:inherit;font-size:10px}.ollama-message-actions button:hover{color:#b8ead9}.ollama-catalog-more{display:flex;justify-content:center;padding:18px}.ollama-message.assistant{border-left-color:rgba(117,210,183,.45)}@media(max-width:1180px){.ollama-chat-shell{grid-template-columns:210px minmax(0,1fr)}.ollama-chat-controls{grid-column:1 / -1;position:static;max-height:none}.ollama-chat-controls .ollama-model-pool{padding:12px;border:1px solid rgba(164,211,199,.16);background:rgba(10,31,28,.7)}}@media(max-width:760px){.ollama-chat-shell{display:flex;flex-direction:column}.ollama-conversation-rail,.ollama-chat-controls{position:static;width:auto;max-height:none}.ollama-chat-main{order:-1}.ollama-chat-main .ollama-conversation{min-height:48vh;max-height:none}.ollama-chat-controls .ollama-model-pool{border:0;padding:0;background:none}.ollama-chat-header{display:block}.ollama-chat-header .ollama-button{margin-top:12px}} \ No newline at end of file +.ollama-thinking-status{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:14px;padding:14px 16px;border:1px solid rgba(117,210,183,.42);border-radius:10px;background:linear-gradient(90deg,rgba(46,111,96,.34),rgba(24,64,57,.5));box-shadow:0 0 20px rgba(74,190,158,.08)}.ollama-thinking-copy{flex:1;min-width:200px}.ollama-thinking-details{flex-basis:100%;padding:12px;border-top:1px solid rgba(164,211,199,.17);color:#a5bfba}.ollama-thinking-detail-grid{display:grid;grid-template-columns:repeat(5,minmax(100px,1fr));gap:8px;margin-bottom:8px}.ollama-thinking-detail-grid span{display:flex;flex-direction:column;gap:3px;padding:8px;border-radius:7px;background:rgba(71,117,108,.11);font-size:10px;color:#8fb5ac}.ollama-thinking-detail-grid strong{color:#e4f4ef;font-size:11px;overflow-wrap:anywhere}.ollama-thinking-details small{font-size:10px;color:#819b96}.ollama-thinking-status .thinking-stop{color:#ffb8b8;border-color:rgba(255,110,110,.45)}.ollama-composer.drop-active{border-color:rgba(117,210,183,.8);background:linear-gradient(135deg,rgba(33,92,79,.52),rgba(23,52,48,.62));box-shadow:0 0 24px rgba(117,210,183,.16)}.ollama-drop-hint{padding:9px;border:1px dashed rgba(117,210,183,.7);border-radius:7px;text-align:center;color:#b8ead9;font-size:11px;background:rgba(117,210,183,.08)}.ollama-thinking-spinner{display:flex;align-items:center;gap:4px;min-width:28px}.ollama-thinking-spinner span{width:7px;height:7px;border-radius:50%;background:#75d2b7;animation:ollama-thinking-pulse 1.1s ease-in-out infinite}.ollama-thinking-spinner span:nth-child(2){animation-delay:.18s}.ollama-thinking-spinner span:nth-child(3){animation-delay:.36s}.ollama-thinking-copy{display:flex;flex-direction:column;gap:3px}.ollama-thinking-copy strong{color:#effcf8;font-size:13px}.ollama-thinking-copy span{color:#b9d8d0;font-size:12px}.ollama-thinking-copy small{color:#8fb5ac;font-size:10px}@keyframes ollama-thinking-pulse{0%,80%,100%{opacity:.35;transform:scale(.8)}40%{opacity:1;transform:scale(1.2)}}.ollama-operations-section{margin-top:30px;padding-top:22px;border-top:1px solid rgba(164,211,199,.22)}.ollama-operations-heading{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:14px;padding:0 2px}.ollama-operations-heading h3{margin:5px 0;color:#effcf8;font-size:18px}.ollama-operations-heading p{margin:0;color:#8fa9a4;font-size:11px;max-width:720px}.ollama-operations-heading>span{padding:7px 9px;border:1px solid rgba(117,210,183,.3);border-radius:999px;color:#9af1c7;font-size:10px;white-space:nowrap}.ollama-advanced-note{padding:10px 0;color:#8fa9a4;font-size:10px;line-height:1.45}.ollama-performance-graphs{margin-top:14px;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-performance-graphs-heading{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;margin-bottom:12px}.ollama-performance-graphs-heading h3{margin:0 0 4px;color:#effcf8}.ollama-performance-graphs-heading p{margin:0;color:#8fa9a4;font-size:11px}.ollama-performance-graphs-heading>span{color:#9af1c7;font-size:10px;white-space:nowrap}.ollama-performance-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.ollama-performance-graph{min-width:0;padding:11px;border:1px solid rgba(155,205,194,.16);border-radius:8px;background:rgba(71,117,108,.11)}.ollama-performance-graph-heading{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}.ollama-performance-graph-heading div{display:flex;flex-direction:column;gap:3px;min-width:0}.ollama-performance-graph-heading strong{color:#dff5ee;font-size:11px}.ollama-performance-graph-heading small{color:#8fa9a4;font-size:9px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ollama-performance-graph-heading b{color:#9af1c7;font-size:12px;white-space:nowrap}.ollama-performance-svg{display:block;width:100%;height:92px;margin-top:7px;overflow:visible}.ollama-graph-grid{stroke:rgba(164,211,199,.16);stroke-width:1}.ollama-graph-area{fill:rgba(117,210,183,.16)}.ollama-graph-line{fill:none;stroke:#75d2b7;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.ollama-performance-graph-scale{display:flex;justify-content:space-between;color:#708e87;font-size:9px}.ollama-performance-graph-scale span:last-child{color:#8fa9a4}@media(max-width:1000px){.ollama-performance-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:760px){.ollama-operations-heading,.ollama-performance-graphs-heading{display:block}.ollama-operations-heading>span,.ollama-performance-graphs-heading>span{display:inline-block;margin-top:10px}.ollama-performance-grid{grid-template-columns:1fr}}.ollama-chat-shell{display:grid;grid-template-columns:240px minmax(0,1fr) 330px;gap:14px;align-items:start;margin-top:14px}.ollama-conversation-rail,.ollama-chat-controls{min-width:0;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-conversation-rail{position:sticky;top:14px;max-height:calc(100vh - 110px);overflow:auto}.ollama-chat-controls{position:sticky;top:14px;max-height:calc(100vh - 110px);overflow:auto}.ollama-rail-heading,.ollama-controls-heading{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;margin-bottom:10px}.ollama-rail-heading h3,.ollama-controls-heading h3{margin:0;font-size:14px}.ollama-rail-heading small,.ollama-controls-heading small{color:#8fa9a4;font-size:10px}.ollama-conversation-rail .ollama-conversation-list{display:grid;gap:6px}.ollama-conversation-rail .ollama-conversation-list .ollama-button{display:flex;justify-content:space-between;gap:8px;width:100%;text-align:left}.ollama-conversation-rail .ollama-conversation-list .ollama-button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ollama-conversation-rail .ollama-conversation-list .ollama-button small{color:#8fa9a4;white-space:nowrap}.ollama-chat-main{display:flex;flex-direction:column;gap:12px;min-width:0}.ollama-chat-main .ollama-conversation{min-height:52vh;max-height:calc(100vh - 310px);overflow:auto;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(7,22,20,.56)}.ollama-chat-main .ollama-composer{position:sticky;bottom:10px;margin-top:0;padding:12px;border:1px solid rgba(141,210,193,.3);border-radius:12px;background:rgba(16,45,41,.96);box-shadow:0 10px 32px rgba(0,0,0,.24)}.ollama-chat-controls>.ollama-model-pool{margin-top:0;padding:0;border:0;background:none}.ollama-chat-controls .ollama-runtime-panel,.ollama-chat-controls .ollama-storage-panel{margin-top:10px;padding:0;border:0;background:none}.ollama-advanced-control{margin-top:12px;padding-top:10px;border-top:1px solid rgba(164,211,199,.14)}.ollama-advanced-control summary{cursor:pointer;color:#b8ead9;font-size:12px;font-weight:700}.ollama-message-meta{display:flex;justify-content:space-between;gap:10px;margin-bottom:6px;color:#8fa9a4;font-size:10px}.ollama-rich-text{line-height:1.55;overflow-wrap:anywhere}.ollama-rich-text>div{min-height:1.2em}.ollama-inline-code{padding:2px 5px;border-radius:4px;background:rgba(0,0,0,.25);color:#c9f4e4;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}.ollama-code-block{overflow:auto;margin:9px 0;padding:11px;border-radius:7px;background:#081714;color:#d7eee7;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.ollama-message-actions{display:flex;gap:6px;margin-top:9px;opacity:.75}.ollama-message-actions button{border:0;background:none;color:#8fb5ac;padding:2px 0;cursor:pointer;font:inherit;font-size:10px}.ollama-message-actions button:hover{color:#b8ead9}.ollama-catalog-more{display:flex;justify-content:center;padding:18px}.ollama-message.assistant{border-left-color:rgba(117,210,183,.45)}@media(max-width:1180px){.ollama-chat-shell{grid-template-columns:210px minmax(0,1fr)}.ollama-chat-controls{grid-column:1 / -1;position:static;max-height:none}.ollama-chat-controls .ollama-model-pool{padding:12px;border:1px solid rgba(164,211,199,.16);background:rgba(10,31,28,.7)}}@media(max-width:760px){.ollama-chat-shell{display:flex;flex-direction:column}.ollama-conversation-rail,.ollama-chat-controls{position:static;width:auto;max-height:none}.ollama-chat-main{order:-1}.ollama-chat-main .ollama-conversation{min-height:48vh;max-height:none}.ollama-chat-controls .ollama-model-pool{border:0;padding:0;background:none}.ollama-chat-header{display:block}.ollama-chat-header .ollama-button{margin-top:12px}} \ No newline at end of file diff --git a/dashboard/manifest.json b/dashboard/manifest.json index 65f9f1f..25fadbf 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "label": "Ollama Models", "description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.", "icon": "Cpu", - "version": "1.7.6", + "version": "1.7.7", "tab": {"path": "/ollama-manager", "position": "after:models"}, "entry": "dist/index.js", "css": "dist/style.css", diff --git a/plugin.yaml b/plugin.yaml index fe342d2..611b93c 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: ollama-manager -version: 1.7.6 +version: 1.7.7 description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry. auto_install_dependencies: true python_dependencies: diff --git a/tests/test_validation_harness.py b/tests/test_validation_harness.py index a7d0d0d..3d859a5 100644 --- a/tests/test_validation_harness.py +++ b/tests/test_validation_harness.py @@ -1,5 +1,6 @@ import threading import unittest +from pathlib import Path from unittest.mock import patch from dashboard import plugin_api as api @@ -219,6 +220,15 @@ class ValidationHarnessTests(unittest.TestCase): self.assertEqual(len(created), 1) self.assertEqual(created[0].history, []) + def test_dashboard_restores_complete_bottom_performance_graph_set(self): + bundle = Path(__file__).parents[1].joinpath("dashboard", "dist", "index.js").read_text(encoding="utf-8") + for label in ("CPU usage", "CPU load", "System memory", "GPU usage", "GPU VRAM", "Disk usage", "Swap usage", "Ollama model weights", "Resident models"): + self.assertIn('title: "' + label + '"', bundle) + self.assertIn("function PerformanceGraphs", bundle) + self.assertIn("ollama-operations-section", bundle) + self.assertIn("historical performance graphs are shown in Operations / Performance below", bundle) + self.assertIn("setInterval(pollRuntime, 1000)", bundle) + if __name__ == "__main__": unittest.main()