Lumi/plugins/lumi_ai/views/settings.ejs
2026-06-16 08:30:41 +02:00

663 lines
52 KiB
Plaintext

<%- include("../../../src/web/views/partials/layout-top", { title }) %>
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=<%= assetVersion %>" />
<%
const renderPresetOptions = (options, current) => {
const value = Number(current);
const hasValue = options.some((option) => option.value === value);
let html = "";
if (!hasValue && Number.isFinite(value)) {
html += `<option value="${value}" selected disabled>Unsupported current value (${value})</option>`;
}
html += options.map((option) => `<option value="${option.value}" ${option.value === value ? "selected" : ""}>${option.label}</option>`).join("");
return html;
};
%>
<section class="ai-titlebar">
<div>
<h1>Lumi AI</h1>
<p>Managed local inference, assistant access, and guarded plugin tools.</p>
</div>
<div class="ai-runtime-badge <%= runtimeStatus.healthy ? 'ready' : 'offline' %>">
<span></span><%= runtimeStatus.healthy ? "Runtime ready" : "Runtime offline" %>
</div>
<button class="button subtle" type="button" data-ai-tools-open>Tools</button>
<a class="button subtle" href="/plugins/lumi_ai/improvement_center">Improvement Center</a>
</section>
<nav class="ai-tabs" aria-label="Lumi AI settings">
<a href="#overview">Overview</a>
<a href="#models">Models</a>
<a href="#runtime">Runtime</a>
<a href="#storage">Storage</a>
<a href="#assistant">Assistant</a>
<a href="#repo-index">Repo index</a>
<a href="#metrics">Metrics</a>
<a href="#logs">Logs</a>
</nav>
<section class="ai-band" id="overview">
<div class="ai-section-heading">
<div><h2>Overview</h2><p>Current installation and host capacity.</p></div>
</div>
<div class="ai-stat-grid">
<div><span>Provider</span><strong>llama.cpp</strong></div>
<div><span>Selected model</span><strong><%= models.find((model) => model.id === config.selected_model_id)?.label || config.selected_model_id %></strong></div>
<div><span>Gate model</span><strong><%= models.find((model) => model.id === config.gate.model_id)?.label || config.gate.model_id %></strong></div>
<div><span>Gate status</span><strong><%= gateStatus.healthy ? "Ready" : gateStatus.state %></strong></div>
<div><span>RAM</span><strong><%= Math.round(hardware.total_ram_mb / 1024) %> GB</strong></div>
<div><span>Free disk</span><strong><%= formatBytes(hardware.free_disk_mb * 1048576) %></strong></div>
<div><span>CPU threads</span><strong><%= hardware.cpu_threads %></strong></div>
<div><span>GPU</span><strong><%= hardware.gpu.present ? hardware.gpu.model : "Not detected" %></strong></div>
<div><span>VRAM</span><strong><%= hardware.gpu.vram_mb ? `${Math.round(hardware.gpu.vram_mb / 1024)} GB` : "Unavailable" %></strong></div>
<div><span>Compute API</span><strong><%= hardware.gpu.compute_api?.length ? hardware.gpu.compute_api.map((api) => api.toUpperCase()).join(", ") : "CPU only" %></strong></div>
<div><span>GPU driver</span><strong><%= hardware.gpu.driver || "Unavailable" %></strong></div>
<div><span>Installed backend</span><strong><%= String(runtimeStatus.runtime_backend || "cpu").toUpperCase() %></strong></div>
<div><span>Recommended backend</span><strong><%= String(hardware.runtime_selection.backend || "cpu").toUpperCase() %></strong></div>
<div><span>Total AI RAM estimate</span><strong><%= formatBytes(resourceEstimate.total_cpu_memory_mb * 1048576) %></strong></div>
<div><span>Total AI VRAM estimate</span><strong><%= formatBytes(resourceEstimate.total_gpu_memory_mb * 1048576) %></strong></div>
</div>
<% resourceEstimate.warnings.forEach((warning) => { %><div class="callout danger"><%= warning %></div><% }) %>
<% sizeDiagnostics.forEach((diagnostic) => { %>
<div class="callout danger"><%= diagnostic.message %></div>
<% }) %>
</section>
<section class="ai-band" id="models">
<div class="ai-section-heading">
<div><h2>Models</h2><p>Pinned GGUF files downloaded directly from Hugging Face and verified by SHA-256.</p></div>
</div>
<div class="ai-model-list">
<% models.forEach((model) => { %>
<article class="ai-model-row">
<div class="ai-model-main">
<strong><%= model.label %></strong>
<span><%= formatBytes(model.size) %> &middot; <%= model.ram_gb %> GB recommended RAM &middot; <%= model.repo %></span>
</div>
<span class="ai-tag <%= model.downloaded ? 'installed' : model.compatible ? '' : 'warning' %>">
<%= model.downloaded ? "Installed" : model.compatible ? "Available" : "Exceeds host" %>
</span>
<div class="ai-inline-form">
<% if (model.downloaded) { %>
<form method="post" action="/plugins/lumi_ai/models/<%= model.id %>/verify">
<button class="button subtle" type="submit">Verify</button>
</form>
<form method="post" action="/plugins/lumi_ai/download/model/<%= model.id %>" data-ai-download-form data-download-id="model:<%= model.id %>">
<%- include("../../../src/web/views/partials/state-button", {
type: "submit",
classes: "subtle",
attrs: `data-ai-download-button data-download-id="model:${model.id}"`,
states: [
{ id: "idle", text: "Redownload" },
{ id: "loading", text: "Downloading", spinner: true },
{ id: "success", text: "Downloaded" },
{ id: "error", text: "Retry" }
]
}) %>
</form>
<form method="post" action="/plugins/lumi_ai/models/<%= model.id %>/delete" data-confirm-title="Delete model" data-confirm-text="Delete <%= model.label %> and recover <%= formatBytes(model.installed_size || model.size) %>?" data-confirm-label="Delete model">
<input type="hidden" name="confirm" value="yes" />
<button class="button danger" type="submit" <%= (model.id === config.selected_model_id && runtimeStatus.state === "running") || (model.id === config.gate.model_id && gateStatus.state === "running") ? "disabled title='Stop the AI runtimes before deleting an active model'" : "" %>>Delete</button>
</form>
<% } else { %>
<form method="post" action="/plugins/lumi_ai/download/model/<%= model.id %>" data-ai-download-form data-download-id="model:<%= model.id %>">
<% if (!model.compatible) { %>
<label title="Allow download despite detected capacity"><input type="checkbox" name="override_compatibility" /> Override</label>
<% } %>
<%- include("../../../src/web/views/partials/state-button", {
type: "submit",
classes: "subtle",
attrs: `data-ai-download-button data-download-id="model:${model.id}"`,
states: [
{ id: "idle", text: "Download" },
{ id: "loading", text: "Downloading", spinner: true },
{ id: "success", text: "Downloaded" },
{ id: "error", text: "Retry" }
]
}) %>
</form>
<% } %>
</div>
</article>
<% }) %>
</div>
</section>
<section class="ai-band" id="runtime">
<div class="ai-section-heading">
<div><h2>Runtime</h2><p>Official llama.cpp release, bound to localhost and stored inside this plugin.</p></div>
<div class="ai-actions" data-ai-runtime-actions>
<%- include("../../../src/web/views/partials/state-button", {
type: "button",
attrs: "data-runtime-primary",
loadingState: "starting",
successState: "running",
errorState: "error",
defaultState: runtimeStatus.state === "running" ? "running" : "idle",
states: [
{ id: "idle", text: "Start" },
{ id: "starting", text: "Starting", spinner: true },
{ id: "running", text: "Restart" },
{ id: "restarting", text: "Restarting", spinner: true },
{ id: "error", text: "Retry" }
]
}) %>
<button class="button subtle" type="button" data-runtime-action="self-test">Run self-test</button>
<button class="button subtle" type="button" data-runtime-action="verify-runtime">Verify runtime</button>
<button class="button subtle" type="button" data-runtime-action="verify-model">Verify model</button>
<button class="button subtle" type="button" data-runtime-action="verify-gate-model">Verify gate model</button>
<button class="button danger" type="button" data-runtime-action="stop">Stop</button>
</div>
</div>
<div class="ai-runtime-grid">
<div class="ai-diagnostic">
<span>Installed</span><strong><%= runtimeStatus.runtime_installed ? "Yes" : "No" %></strong>
<span>Process</span><strong data-runtime-state><%= runtimeStatus.state %></strong>
<span>Health</span><strong><%= runtimeStatus.healthy ? "Healthy" : "Unavailable" %></strong>
<span>PID</span><strong><%= runtimeStatus.pid || "None" %></strong>
<span>Last stop</span><strong><%= runtimeState.last_stop_reason %></strong>
<span>Platform</span><strong><%= hardware.platform %>-<%= hardware.architecture %></strong>
<span>Self-test</span><strong><%= runtimeStatus.last_self_test?.success ? "Passed" : runtimeStatus.last_self_test ? "Failed" : "Not run" %></strong>
<span>Runtime folder</span><strong><%= formatBytes(runtimeFolderSize) %></strong>
<span>Runtime archive</span><strong><%= runtimeTarget ? formatBytes(runtimeTarget.size) : "Unavailable" %></strong>
<span>Model installed</span><strong><%= formatBytes(modelFileSize) %></strong>
<span>Model download</span><strong><%= formatBytes(models.find((model) => model.id === config.selected_model_id)?.size || 0) %></strong>
<span>Backend</span><strong><%= String(runtimeStatus.runtime_backend || "cpu").toUpperCase() %></strong>
<span>GPU intent</span><strong><%= runtimeStatus.gpu_allocation_intent_percent || 0 %>%</strong>
<span>GPU actual</span><strong><%= runtimeStatus.gpu_allocation_actual_percent || 0 %>%</strong>
<span>GPU safe maximum</span><strong><%= runtimeStatus.gpu_allocation_max_safe_percent || 0 %>%</strong>
<span>GPU layers</span><strong><%= runtimeStatus.gpu_layers || 0 %></strong>
<span>Total VRAM</span><strong><%= formatBytes((runtimeStatus.total_vram_mb || 0) * 1048576) %></strong>
<span>Free VRAM</span><strong><%= formatBytes((runtimeStatus.free_vram_mb || 0) * 1048576) %></strong>
<span>Managed model VRAM</span><strong><%= formatBytes((runtimeStatus.managed_model_vram_mb || 0) * 1048576) %></strong>
<span>External VRAM estimate</span><strong><%= formatBytes((runtimeStatus.external_vram_estimate_mb || 0) * 1048576) %></strong>
</div>
<div>
<p><strong>Lightweight gate: <%= gateStatus.healthy ? "Ready" : gateStatus.state %></strong></p>
<p class="hint">
<%= gateStatus.model_id || config.gate.model_id %> &middot;
CPU <%= formatBytes((gateStatus.estimated_cpu_memory_mb || 0) * 1048576) %> &middot;
VRAM <%= formatBytes((gateStatus.estimated_gpu_memory_mb || 0) * 1048576) %>
</p>
<% if (gateStatus.last_error) { %><div class="callout"><%= gateStatus.last_error %></div><% } %>
<% if (runtimeTarget) { %>
<p><strong>Managed <%= String(runtimeTarget.backend || "cpu").toUpperCase() %> release <%= runtimeManifest?.version || "b9592" %></strong></p>
<p class="hint"><%= runtimeTarget.filename %> &middot; <%= formatBytes(runtimeTarget.size) %></p>
<form method="post" action="/plugins/lumi_ai/download/runtime" data-ai-download-form data-download-id="runtime">
<%- include("../../../src/web/views/partials/state-button", {
type: "submit",
classes: "subtle",
attrs: "data-ai-download-button data-download-id=\"runtime\"",
states: [
{ id: "idle", text: runtimeStatus.runtime_installed ? "Reinstall runtime" : "Download runtime" },
{ id: "loading", text: "Downloading", spinner: true },
{ id: "success", text: "Downloaded" },
{ id: "error", text: "Retry" }
]
}) %>
</form>
<% } else { %>
<div class="callout">No managed runtime build is available for this OS and architecture.</div>
<% } %>
<% if (runtimeStatus.last_error) { %><div class="callout danger"><%= runtimeStatus.last_error %></div><% } %>
<% if (runtimeStatus.acceleration_warning) { %><div class="callout"><%= runtimeStatus.acceleration_warning %></div><% } %>
<% tuningHints.forEach((hint) => { %><div class="callout"><%= hint %></div><% }) %>
<% if (hardware.runtime_selection.fallback_to_cpu) { %><div class="callout"><%= hardware.runtime_selection.reason %></div><% } %>
</div>
</div>
<div class="ai-download-status" data-download-status hidden></div>
</section>
<section class="ai-band" id="runtime-diagnostics">
<div class="ai-section-heading">
<div><h2>Runtime diagnostics</h2><p>Latest plugin-local runtime failure and remediation details.</p></div>
<a class="button subtle" href="/plugins/lumi_ai/diagnostics/download">Download diagnostics</a>
</div>
<% if (latestDiagnostic) { %>
<div class="callout danger">
<strong><%= latestDiagnostic.code %>: <%= latestDiagnostic.message %></strong>
<p><%= latestDiagnostic.category %> / <%= latestDiagnostic.severity %></p>
</div>
<% if (latestDiagnostic.remediation_steps?.length) { %>
<ol class="ai-remediation"><% latestDiagnostic.remediation_steps.forEach((step) => { %><li><%= step %></li><% }) %></ol>
<% } %>
<details class="ai-raw-diagnostic">
<summary>Raw diagnostic details</summary>
<pre><%= JSON.stringify(latestDiagnostic, null, 2) %></pre>
</details>
<% } else { %>
<p class="hint">No runtime diagnostic has been recorded.</p>
<% } %>
<% if (hardware.network_path_warning) { %>
<div class="callout">The plugin path may be a mapped or network-like location. A local disk path is more reliable for native runtime DLL loading.</div>
<% } %>
<% if (hardware.long_path_warning) { %><div class="callout">The plugin path is unusually long for Windows native loading. Consider a shorter local installation path.</div><% } %>
</section>
<section class="ai-band" id="storage">
<div class="ai-section-heading">
<div><h2>Storage cleanup</h2><p>Plugin-local files only. Selected models and active runtimes are protected.</p></div>
<strong><%= formatBytes(storageUsage.total) %> total</strong>
</div>
<div class="ai-stat-grid compact">
<% Object.entries(storageUsage.categories).forEach(([category, bytes]) => { %>
<div><span><%= category.replace("_", " ") %></span><strong><%= formatBytes(bytes) %></strong></div>
<% }) %>
</div>
<form method="post" action="/plugins/lumi_ai/storage/cleanup" class="ai-cleanup-form" data-confirm-title="Clean AI storage" data-confirm-text="Delete the selected plugin-local storage categories?" data-confirm-label="Clean selected">
<label><input type="checkbox" name="categories" value="unused_models" /> Unused models</label>
<label><input type="checkbox" name="categories" value="runtime_archives" /> Runtime archives</label>
<label><input type="checkbox" name="categories" value="logs" /> Old logs</label>
<label><input type="checkbox" name="categories" value="metrics" /> Metrics history</label>
<label><input type="checkbox" name="categories" value="diagnostics" /> Diagnostics</label>
<label><input type="checkbox" name="categories" value="cache" /> Cache</label>
<label><input type="checkbox" name="categories" value="tmp" /> Temporary files</label>
<label><input type="checkbox" name="categories" value="runtime" <%= runtimeStatus.state === "running" ? "disabled" : "" %> /> Extracted runtime</label>
<button class="button danger" type="submit">Clean selected</button>
</form>
</section>
<form method="post" action="/plugins/lumi_ai/settings" data-lumi-settings-form>
<section class="ai-band" id="assistant">
<div class="ai-section-heading">
<div><h2>Assistant</h2><p>Configuration remains admin-only. Visibility controls only the sidebar assistant.</p></div>
<button class="button" type="submit">Save settings</button>
</div>
<div class="ai-settings-groups">
<details class="ai-settings-group" open>
<summary>Model, runtime, and GPU</summary>
<div class="form-grid ai-form">
<div class="field">
<label>AI enabled</label>
<label class="switch"><input class="switch-input" type="checkbox" name="enabled" <%= config.enabled ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Available</span></label>
</div>
<div class="field">
<label>Sidebar assistant</label>
<label class="switch"><input class="switch-input" type="checkbox" name="assistant_enabled" <%= config.assistant_enabled !== false ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Enabled</span></label>
</div>
<div class="field">
<label for="selected-model">Selected model</label>
<% if (!selectedModelInstalled) { %>
<div class="callout danger">The currently selected model is not installed. Choose an installed model before saving.</div>
<% } %>
<select id="selected-model" name="selected_model_id" data-gpu-model required>
<% if (!installedModels.length) { %>
<option value="">No installed models available</option>
<% } %>
<% installedModels.forEach((model) => { %><option value="<%= model.id %>" <%= model.id === config.selected_model_id ? "selected" : "" %>><%= model.label %></option><% }) %>
</select>
</div>
<div class="field">
<label>Context size</label>
<select name="context_size" data-gpu-context>
<% contextOptions.forEach((option) => { %>
<option value="<%= option.value %>" <%= Number(config.context_size) === option.value ? "selected" : "" %>><%= option.label %> - <%= option.description %></option>
<% }) %>
</select>
</div>
<div class="field"><label>CPU threads (0 = auto)</label><input type="number" name="threads" min="0" max="256" value="<%= config.threads %>" /></div>
<div class="field full ai-gpu-control" data-gpu-control data-endpoint="/plugins/lumi_ai/api/gpu-capacity">
<div class="ai-gpu-label">
<label for="gpu-workload">GPU Acceleration</label>
<strong data-gpu-value><%= gpuAllocation.gpu_allocation_intent_percent %>% intent</strong>
</div>
<div class="ai-gpu-slider" style="--gpu-actual: <%= gpuAllocation.gpu_allocation_actual_percent %>%; --gpu-max: <%= gpuAllocation.gpu_allocation_max_safe_percent %>%;" data-gpu-slider title="Maximum safe allocation: <%= gpuAllocation.gpu_allocation_max_safe_percent %>%">
<input id="gpu-workload" type="range" name="gpu_allocation_intent_percent" min="0" max="100" step="1" value="<%= gpuAllocation.gpu_allocation_intent_percent %>" data-gpu-workload />
</div>
<div class="ai-gpu-scale"><span>CPU only</span><span data-gpu-limit>Maximum safe: <%= gpuAllocation.gpu_allocation_max_safe_percent %>%</span><span>Maximum GPU</span></div>
<div class="ai-gpu-summary">
<span>Backend <strong data-gpu-backend><%= String(gpuAllocation.backend).toUpperCase() %></strong></span>
<span>Intended <strong data-gpu-intent><%= gpuAllocation.gpu_allocation_intent_percent %>%</strong></span>
<span>Actual <strong data-gpu-actual><%= gpuAllocation.gpu_allocation_actual_percent %>%</strong></span>
<span>Managed model VRAM <strong data-gpu-memory><%= formatBytes(gpuAllocation.managed_model_vram_mb * 1048576) %></strong></span>
<span>Total VRAM <strong data-gpu-total-vram><%= formatBytes(gpuAllocation.total_vram_mb * 1048576) %></strong></span>
<span>Free VRAM <strong data-gpu-free-vram><%= formatBytes(gpuAllocation.free_vram_mb * 1048576) %></strong></span>
<span>External VRAM <strong data-gpu-external-vram><%= formatBytes(gpuAllocation.external_vram_estimate_mb * 1048576) %></strong></span>
</div>
<p class="hint" data-gpu-warning <%= gpuAllocation.warning ? "" : "hidden" %>><%= gpuAllocation.warning || "" %></p>
</div>
<div class="field"><label>Concurrent requests</label><input type="number" name="concurrency" min="1" max="8" value="<%= config.concurrency %>" /></div>
<div class="field"><label>Maximum queue</label><input type="number" name="max_queue_length" min="1" max="100" value="<%= config.max_queue_length %>" /></div>
<div class="field"><label>UI soft timeout (ms)</label><input type="number" name="ui_soft_timeout_ms" min="5000" max="300000" value="<%= config.ui_soft_timeout_ms %>" /><span class="hint">Shows Continue waiting controls without stopping the job.</span></div>
<div class="field"><label>Hard generation timeout (ms)</label><input type="number" name="hard_generation_timeout_ms" min="30000" max="3600000" value="<%= config.hard_generation_timeout_ms %>" /></div>
<div class="field"><label>API/test output token fallback</label><select name="max_output_tokens"><%- renderPresetOptions(tokenPresets, config.max_output_tokens) %></select><span class="hint">Normal assistant requests use the class budgets below.</span></div>
<div class="field"><label>Navigation/help tokens</label><select name="output_budget_navigation_help"><%- renderPresetOptions(tokenPresets, config.output_budgets.navigation_help) %></select></div>
<div class="field"><label>Simple answer tokens</label><select name="output_budget_simple_answer"><%- renderPresetOptions(tokenPresets, config.output_budgets.simple_answer) %></select></div>
<div class="field"><label>Code/custom command tokens</label><select name="output_budget_code_custom_command"><%- renderPresetOptions(tokenPresets, config.output_budgets.code_custom_command) %></select></div>
<div class="field"><label>Admin debug tokens</label><select name="output_budget_admin_debug"><%- renderPresetOptions(tokenPresets, config.output_budgets.admin_debug) %></select></div>
<div class="field"><label>Explicit long-answer tokens</label><select name="output_budget_explicit_long"><%- renderPresetOptions(tokenPresets, config.output_budgets.explicit_long) %></select></div>
<div class="field"><label>Batch size</label><input type="number" name="batch_size" min="32" max="4096" value="<%= config.batch_size %>" /></div>
<div class="field"><label>Micro-batch size</label><input type="number" name="ubatch_size" min="16" max="4096" value="<%= config.ubatch_size %>" /></div>
<div class="field"><label>Requests per user/minute</label><input type="number" name="per_user_requests_per_minute" min="1" max="120" value="<%= config.per_user_requests_per_minute %>" /></div>
<div class="field"><label>Admin rate-limit bypass</label><label class="switch"><input class="switch-input" type="checkbox" name="admin_bypass_rate_limit" <%= config.admin_bypass_rate_limit ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Bypass</span></label></div>
</div>
</details>
<details class="ai-settings-group" open>
<summary>Lightweight request gate</summary>
<div class="form-grid ai-form">
<div class="field">
<label>Gate model</label>
<select name="gate_model_id"><% models.forEach((model) => { %><option value="<%= model.id %>" <%= model.id === config.gate.model_id ? "selected" : "" %>><%= model.label %></option><% }) %></select>
<span class="hint">Use the smallest downloaded model that can reliably return JSON classifications.</span>
</div>
<div class="field"><label>Gate context size</label><select name="gate_context_size"><%- renderPresetOptions(gateContextOptions, config.gate.context_size) %></select></div>
<div class="field"><label>Gate CPU threads</label><input type="number" name="gate_threads" min="1" max="16" value="<%= config.gate.threads %>" /></div>
<div class="field"><label>Gate timeout (ms)</label><input type="number" name="gate_timeout_ms" min="1000" max="5000" step="250" value="<%= config.gate.timeout_ms %>" /><span class="hint">Timeout or errors immediately escalate to the main model.</span></div>
<div class="field"><label>High-confidence threshold</label><input type="number" name="gate_high_confidence_threshold" min="0.5" max="0.99" step="0.01" value="<%= config.gate.high_confidence_threshold %>" /></div>
<div class="field"><label>Main LLM threshold</label><input type="number" name="gate_main_llm_threshold" min="0.1" max="0.95" step="0.01" value="<%= config.gate.main_llm_threshold %>" /></div>
<div class="field"><label>Safe cache and predefined answers</label><label class="switch"><input class="switch-input" type="checkbox" name="gate_predefined_enabled" <%= config.gate.predefined_enabled ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Enabled</span></label></div>
<div class="field"><label>Cache TTL (seconds)</label><input type="number" name="gate_cache_ttl_seconds" min="30" max="604800" value="<%= config.gate.cache_ttl_seconds %>" /></div>
<div class="field"><label>Repeat force window (seconds)</label><input type="number" name="gate_repeat_force_window_seconds" min="0" max="3600" value="<%= config.gate.repeat_force_window_seconds %>" /></div>
<div class="field"><label>Similarity threshold</label><input type="number" name="gate_similarity_threshold" min="0.5" max="1" step="0.01" value="<%= config.gate.similarity_threshold %>" /></div>
<div class="field"><label>Explicit force prefix</label><input name="gate_force_prefix" maxlength="40" value="<%= config.gate.force_prefix %>" placeholder="force ai:" /></div>
<div class="field full"><span class="hint">The gate cannot execute tools. Permission-sensitive, ambiguous, complex, low-confidence, and repeated requests are sent to the main model.</span></div>
</div>
</details>
<details class="ai-settings-group" open>
<summary>Assistant visibility and diagnostics</summary>
<div class="form-grid ai-form">
<fieldset class="field full ai-fieldset">
<legend>Sidebar visibility</legend>
<label><input type="checkbox" name="visibility_admins" <%= config.assistant_visibility.admins ? "checked" : "" %> /> Administrators</label>
<label><input type="checkbox" name="visibility_mods" <%= config.assistant_visibility.mods ? "checked" : "" %> /> Moderators</label>
<label><input type="checkbox" name="visibility_users" <%= config.assistant_visibility.users ? "checked" : "" %> /> Users</label>
</fieldset>
<div class="field full ai-assistant-diagnostic" data-assistant-diagnostics data-endpoint="/api/lumi-ai/assistant/visibility-debug">
<div class="ai-diagnostic-summary">
<strong>Assistant pill: <span data-assistant-status><%= visibilityDiagnostics.available ? "Backend ready" : "Hidden" %></span></strong>
<span data-assistant-reason><%= assistantReason %></span>
</div>
<div class="ai-condition-grid" data-assistant-conditions>
<% visibilityDiagnostics.conditions.forEach((condition) => { %>
<div data-condition="<%= condition.key %>">
<span><%= condition.key.replaceAll("_", " ") %></span>
<strong class="<%= condition.passed ? 'pass' : 'fail' %>"><%= condition.passed ? "Pass" : "Fail" %></strong>
</div>
<% }) %>
</div>
<div class="ai-panel-render-diagnostic">
<span>User ID <strong><%= visibilityDiagnostics.permission.debug_details.resolved_user_id || "None" %></strong></span>
<span>Role <strong><%= visibilityDiagnostics.permission.normalized_role %></strong></span>
<span>Role source <strong><%= visibilityDiagnostics.permission.debug_details.role_source %></strong></span>
<span>Allowed roles <strong><%= visibilityDiagnostics.permission.debug_details.allowed_roles.join(", ") || "None" %></strong></span>
<span>Origin <strong><%= visibilityDiagnostics.permission.debug_details.origin %></strong></span>
<span>Endpoint status <strong data-panel-endpoint-status><%= panelDiagnostics.panel_endpoint_status || "Not requested" %></strong></span>
<span>HTML length <strong data-panel-html-length><%= panelDiagnostics.panel_html_length || 0 %></strong></span>
<span>Template <strong><%= panelDiagnostics.panel_template_path %></strong></span>
<span>Missing locals <strong><%= panelDiagnostics.missing_locals.length ? panelDiagnostics.missing_locals.join(", ") : "None" %></strong></span>
<span>HTML error <strong data-panel-html-error><%= panelDiagnostics.panel_html_error || "None" %></strong></span>
<span>Mount error <strong data-panel-mount-error><%= panelDiagnostics.mount_error || "None" %></strong></span>
</div>
</div>
</div>
</details>
<details class="ai-settings-group">
<summary>Improvement Center</summary>
<div class="form-grid ai-form">
<div class="field">
<label>Moderator response review</label>
<label class="switch"><input class="switch-input" type="checkbox" name="allow_moderators_to_review_responses" <%= config.improvement.allow_moderators_to_review_responses ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Enabled</span></label>
</div>
<div class="field">
<label>Approved corrections</label>
<label class="switch"><input class="switch-input" type="checkbox" name="corrections_enabled" <%= config.improvement.corrections_enabled ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Enabled</span></label>
</div>
<div class="field full">
<label>Trusted moderator reviewer IDs</label>
<textarea name="trusted_moderator_reviewers" rows="2" placeholder="One internal user ID per line"><%= config.improvement.trusted_moderator_reviewers.join("\n") %></textarea>
<span class="hint">Trusted moderators can verify reviews. Only administrators can approve, edit, delete, promote, export, or run evals.</span>
</div>
<div class="field full"><a class="button subtle" href="/plugins/lumi_ai/improvement_center">Open Improvement Center</a></div>
</div>
</details>
<details class="ai-settings-group">
<summary>Assistant identity and scope</summary>
<div class="form-grid ai-form">
<div class="field full ai-identity-preview"><strong>Lumi Assistant</strong><span>Built-in AI assistant for Lumi. This identity is fixed and cannot be replaced by model branding.</span></div>
<div class="field full"><label>Allowed topics</label><textarea name="allowed_topics" rows="3"><%= config.support_scope.allowed_topics %></textarea></div>
<div class="field full"><label>Allowed support domains</label><textarea name="allowed_support_domains" rows="3"><%= config.support_scope.allowed_support_domains %></textarea></div>
<div class="field full"><label>Answer style</label><textarea name="answer_style" rows="2"><%= config.support_scope.answer_style %></textarea></div>
<div class="field full"><label>Linking behavior</label><textarea name="linking_behavior" rows="2"><%= config.support_scope.linking_behavior %></textarea></div>
<div class="field full"><label>Clarification behavior</label><textarea name="clarification_behavior" rows="2"><%= config.support_scope.clarification_behavior %></textarea></div>
<div class="field full"><label>Out-of-scope response</label><textarea name="out_of_scope_response" rows="2"><%= config.instructions.out_of_scope_response %></textarea></div>
<div class="field"><label>Preferred final answer length</label><input type="number" name="maximum_answer_length" min="100" max="4000" value="<%= config.support_scope.max_answer_length %>" /><span class="hint">Style guidance for the final answer. It does not limit prompt context or reasoning.</span></div>
<div class="field"><label>Internal generation character budget</label><input type="number" name="internal_generation_char_budget" min="2000" max="64000" step="1000" value="<%= config.internal_generation_char_budget %>" /><span class="hint">Controls model output capacity, not prompt or context length.</span></div>
<div class="field"><label>Roleplay intensity (0-10)</label><input type="number" name="roleplay_intensity" min="0" max="10" value="<%= config.instructions.roleplay_intensity || 0 %>" /></div>
<div class="field full"><label>Community tone</label><textarea name="community_tone" rows="2"><%= config.instructions.community_tone %></textarea></div>
<div class="field full"><label>Administrator scope override</label><textarea name="scope_admin" rows="2"><%= config.support_scope.role_overrides.admin %></textarea></div>
<div class="field full"><label>Moderator scope override</label><textarea name="scope_mod" rows="2"><%= config.support_scope.role_overrides.mod %></textarea></div>
<div class="field full"><label>User scope override</label><textarea name="scope_user" rows="2"><%= config.support_scope.role_overrides.user %></textarea></div>
<div class="field full"><label>Admin custom instructions</label><textarea name="admin_custom" rows="4"><%= config.instructions.admin_custom %></textarea><span class="hint">Hard scope, role, tool, and confirmation rules cannot be overridden.</span></div>
<div class="field full ai-hard-scope">
<strong>Hard scope</strong>
<ul><% hardRules.forEach((rule) => { %><li><%= rule %></li><% }) %></ul>
</div>
</div>
</details>
<details class="ai-settings-group">
<summary>Platform commands</summary>
<div class="form-grid ai-form">
<div class="field"><label>Commands</label><label class="switch"><input class="switch-input" type="checkbox" name="command_enabled" <%= config.commands.enabled ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Enabled</span></label></div>
<div class="field"><label>Triggers and aliases</label><input name="command_triggers" value="<%= config.commands.triggers.join(', ') %>" /></div>
<fieldset class="field full ai-fieldset"><legend>Platforms</legend>
<% Object.entries(config.commands.platforms).forEach(([platform, enabled]) => { %>
<label><input type="checkbox" name="command_platform_<%= platform %>" <%= enabled ? "checked" : "" %> /> <%= platform %></label>
<% }) %>
</fieldset>
<fieldset class="field full ai-fieldset"><legend>Roles</legend>
<label><input type="checkbox" name="command_role_admins" <%= config.commands.roles.admins ? "checked" : "" %> /> Administrators</label>
<label><input type="checkbox" name="command_role_mods" <%= config.commands.roles.mods ? "checked" : "" %> /> Moderators</label>
<label><input type="checkbox" name="command_role_users" <%= config.commands.roles.users ? "checked" : "" %> /> Users</label>
</fieldset>
<div class="field full"><label>Runtime unavailable reply</label><input name="command_unavailable_message" value="<%= config.commands.unavailable_message %>" /></div>
<div class="field full"><label>Access denied reply</label><input name="command_denied_message" value="<%= config.commands.denied_message %>" /></div>
</div>
</details>
<details class="ai-settings-group">
<summary>Rate limits</summary>
<div class="ai-limit-grid">
<% [
["limit_role_admin","Administrator role",config.rate_limits.roles.admin],
["limit_role_mod","Moderator role",config.rate_limits.roles.mod],
["limit_role_user","User role",config.rate_limits.roles.user],
["limit_platform_webui","WebUI",config.rate_limits.platforms.webui],
["limit_platform_discord","Discord",config.rate_limits.platforms.discord],
["limit_platform_twitch","Twitch",config.rate_limits.platforms.twitch],
["limit_platform_youtube","YouTube",config.rate_limits.platforms.youtube],
["limit_platform_kick","Kick",config.rate_limits.platforms.kick],
["limit_platform_other","Other platform",config.rate_limits.platforms.other],
["limit_user","Per user",config.rate_limits.per_user],
["limit_channel","Per channel/server",config.rate_limits.per_channel]
].forEach(([key,label,limit]) => { %>
<div><strong><%= label %></strong><label>Requests <input type="number" min="0" max="10000" name="<%= key %>_requests" value="<%= limit.requests %>" /></label><label>Window seconds <input type="number" min="1" max="86400" name="<%= key %>_window" value="<%= limit.window_seconds %>" /></label></div>
<% }) %>
</div>
</details>
<details class="ai-settings-group">
<summary>Support diagnostics and logging</summary>
<div class="form-grid ai-form">
<fieldset class="field full ai-fieldset">
<legend>Support diagnostics</legend>
<label><input type="checkbox" name="repo_lookup_enabled" <%= config.support_scope.repo_lookup_enabled ? "checked" : "" %> /> Use local repository index</label>
<label><input type="checkbox" name="allow_deterministic_help_shortcuts" <%= config.support_scope.allow_deterministic_help_shortcuts ? "checked" : "" %> /> Allow exact deterministic navigation shortcuts</label>
<label><input type="checkbox" name="allow_moderator_code_help" <%= config.support_scope.allow_moderator_code_help ? "checked" : "" %> /> Allow moderators to receive code and custom JavaScript help</label>
<label><input type="checkbox" name="assistant_debug_logging" <%= config.assistant_debug_logging ? "checked" : "" %> /> Browser console diagnostics</label>
</fieldset>
<fieldset class="field full ai-fieldset">
<legend>Logging</legend>
<% [["log_prompts","Prompts"],["log_responses","Responses"],["log_tool_calls","Tool calls"],["log_metrics","Metrics"],["log_internal_audit","Internal audit"]].forEach(([key,label]) => { %>
<label><input type="checkbox" name="<%= key %>" <%= config.logging[key] ? "checked" : "" %> /> <%= label %></label>
<% }) %>
</fieldset>
</div>
</details>
</div>
</section>
</form>
<section class="ai-band" id="ai-access">
<div class="ai-section-heading"><div><h2>User AI access</h2><p>Bans and timeouts apply to WebUI and platform commands.</p></div></div>
<form method="post" action="/plugins/lumi_ai/access-control" class="ai-access-form" data-ai-access-form>
<div class="field ai-user-picker">
<label>User</label>
<input type="search" autocomplete="off" placeholder="Search name, username, platform ID, or user ID" data-user-search />
<input type="hidden" name="user_id" required data-user-id />
<div class="ai-user-results" data-user-results hidden></div>
<div class="ai-user-preview" data-user-preview hidden></div>
</div>
<div class="field"><label>Action</label><select name="action" data-access-action><option value="ban">Ban</option><option value="timeout">Timeout</option><option value="remove">Remove restriction</option></select></div>
<div class="field" data-timeout-field hidden><label>Timeout until</label><input type="datetime-local" name="timeout_until" /></div>
<div class="field"><label>Reason</label><input name="reason" /></div>
<div class="field"><label><input type="checkbox" name="silent" /> Silently ignore platform commands</label></div>
<div class="field"><button class="button" type="submit">Update access</button></div>
</form>
<div class="table-tools"><input type="search" class="table-search" placeholder="Search restrictions" data-table-filter="ai-restrictions" /></div>
<div class="table-wrap"><table class="table" data-table="ai-restrictions"><thead><tr><th>User</th><th>Restriction</th><th>Until</th><th>Reason</th></tr></thead><tbody>
<% activeAiRestrictions.forEach((entry) => { %><tr data-search="<%= `${entry.user_id} ${entry.banned ? "banned" : "timed out"} ${entry.reason || ""}`.toLowerCase() %>"><td><%= entry.user_id %></td><td><%= entry.banned ? "Banned" : "Timed out" %></td><td><%= entry.timeout_until ? formatDate(entry.timeout_until) : "-" %></td><td><%= entry.reason || "-" %></td></tr><% }) %>
<% if (!activeAiRestrictions.length) { %><tr><td colspan="4">No active AI restrictions.</td></tr><% } %>
</tbody></table></div>
<div class="table-pagination">
<a class="button subtle <%= accessPage.page <= 1 ? 'disabled' : '' %>" href="?access_page=<%= Math.max(1, accessPage.page - 1) %>#ai-access">Previous</a>
<span class="table-page-label">Page <%= accessPage.page %> of <%= accessPage.pages %> (<%= accessPage.total %> entries)</span>
<a class="button subtle <%= accessPage.page >= accessPage.pages ? 'disabled' : '' %>" href="?access_page=<%= Math.min(accessPage.pages, accessPage.page + 1) %>#ai-access">Next</a>
</div>
<details class="ai-settings-group"><summary>Recent rate-limit denials</summary>
<div class="table-wrap"><table class="table"><thead><tr><th>Time</th><th>User</th><th>Platform</th><th>Bucket</th><th>Retry</th></tr></thead><tbody>
<% recentRateLimitDenials.forEach((entry) => { %><tr><td><%= formatDate(entry.at) %></td><td><%= entry.user_id %></td><td><%= entry.platform %></td><td><%= entry.bucket %></td><td><%= entry.retry_after_seconds %>s</td></tr><% }) %>
<% if (!recentRateLimitDenials.length) { %><tr><td colspan="5">No recent rate-limit denials.</td></tr><% } %>
</tbody></table></div>
</details>
</section>
<section class="ai-band" id="repo-index">
<div class="ai-section-heading">
<div><h2>Repository support index</h2><p>Local Lumi routes, settings pages, plugin manifests, commands, and documentation.</p></div>
<div class="ai-actions">
<form method="post" action="/plugins/lumi_ai/repo-index/refresh">
<input type="hidden" name="source" value="local" />
<button class="button subtle" type="submit">Refresh local</button>
</form>
<form method="post" action="/plugins/lumi_ai/repo-index/refresh" data-confirm-title="Refresh public repository index" data-confirm-text="Download and index the approved public Lumi repository?" data-confirm-label="Refresh public">
<input type="hidden" name="source" value="public" />
<button class="button subtle" type="submit">Refresh public</button>
</form>
</div>
</div>
<div class="ai-stat-grid compact">
<div><span>Status</span><strong><%= repoIndexStatus.present ? repoIndexStatus.stale ? "Stale" : "Ready" : "Missing" %></strong></div>
<div><span>Last indexed</span><strong><%= repoIndexStatus.indexed_at ? formatDate(repoIndexStatus.indexed_at) : "Never" %></strong></div>
<div><span>Commit</span><strong><%= repoIndexStatus.commit ? repoIndexStatus.commit.slice(0, 12) : "Unavailable" %></strong></div>
<div><span>Routes</span><strong><%= repoIndexStatus.route_count %></strong></div>
<div><span>Plugins</span><strong><%= repoIndexStatus.plugin_count %></strong></div>
<div><span>Commands</span><strong><%= repoIndexStatus.command_count %></strong></div>
</div>
</section>
<section class="ai-band" id="test-console">
<div class="ai-section-heading"><div><h2>Test console</h2><p>Run a request as a simulated role without changing the logged-in actor.</p></div></div>
<form class="form-grid ai-form" data-ai-test-form>
<div class="field"><label>Simulated role</label><select name="role"><option value="admin">Admin</option><option value="mod">Moderator</option><option value="user">User</option></select></div>
<div class="field"><label>Origin</label><select name="origin"><option value="webui">WebUI</option><option value="discord">Discord</option><option value="twitch">Twitch</option><option value="youtube">YouTube</option><option value="kick">Kick</option><option value="other">Other</option></select></div>
<div class="field full"><label>Message</label><textarea name="message" rows="3" required>Where can I find Twitch configuration?</textarea></div>
<div class="field full ai-fieldset">
<label><input type="checkbox" name="allow_tools" /> Allow registered tools through the normal generation pipeline</label>
<label><input type="checkbox" name="show_raw_prompt" /> Show assembled prompt</label>
<label><input type="checkbox" name="show_raw_output" /> Show raw model response</label>
</div>
<div class="field full"><p class="hint" data-ai-test-tools-notice>Tools are disabled for this test unless explicitly enabled above.</p></div>
<div class="field full"><button class="button" type="submit">Run test</button></div>
</form>
<pre class="ai-test-output" data-ai-test-output hidden></pre>
</section>
<section class="ai-band" id="metrics">
<div class="ai-section-heading"><div><h2>Metrics</h2><p>Plugin-local operational counters and recent requests.</p></div></div>
<div class="ai-stat-grid compact">
<div><span>Requests</span><strong><%= metrics.total_requests %></strong></div>
<div><span>Successful</span><strong><%= metrics.successful %></strong></div>
<div><span>Failed</span><strong><%= metrics.failed %></strong></div>
<div><span>Refused</span><strong><%= metrics.refusals %></strong></div>
<div><span>Gate decisions</span><strong><%= metrics.gate_decisions || 0 %></strong></div>
<div><span>Average</span><strong><%= formatDuration(metrics.average_response_ms) %></strong></div>
<div><span>Median</span><strong><%= formatDuration(metrics.median_response_ms) %></strong></div>
<div><span>Avg gate</span><strong><%= formatDuration(metrics.average_stage_ms?.gate_ms || 0) %></strong></div>
<div><span>Avg main generation</span><strong><%= formatDuration(metrics.average_stage_ms?.main_generate_ms || 0) %></strong></div>
</div>
<details class="ai-settings-group">
<summary>Current and recent assistant jobs</summary>
<div class="table-wrap">
<table class="table"><thead><tr><th>Created</th><th>State / stage</th><th>Class / budget</th><th>Elapsed</th><th>Gate</th><th>Queue</th><th>Prompt eval</th><th>Generation</th><th>Tokens</th><th>Speed</th><th>Runtime</th><th>UI timeout</th></tr></thead><tbody>
<% jobDiagnostics.forEach((job) => { %><tr><td><%= formatDate(job.created_at) %></td><td><%= job.state %> / <%= job.stage %></td><td><%= job.details.route_class || "-" %> / <%= job.details.max_output_tokens_used || job.details.max_output_tokens || "-" %></td><td><%= formatDuration(job.elapsed_ms) %></td><td><%= formatDuration(job.details.gate_ms) %></td><td><%= formatDuration(job.details.queue_ms) %></td><td><%= formatDuration(job.details.prompt_eval_ms) %></td><td><%= formatDuration(job.details.generation_ms) %></td><td><%= job.details.prompt_tokens || 0 %> / <%= job.details.generated_tokens || 0 %></td><td><%= job.details.prompt_tps || 0 %> / <%= job.details.generation_tps || 0 %> tok/s</td><td><%= job.details.backend || "-" %>, <%= job.details.gpu_layers || 0 %> layers, ctx <%= job.details.context_size || "-" %></td><td><%= job.frontend_soft_timeout_at ? (job.still_running ? "Still running" : "Recorded") : "No" %></td></tr><% }) %>
<% if (!jobDiagnostics.length) { %><tr><td colspan="12">No assistant jobs recorded since this plugin process started.</td></tr><% } %>
</tbody></table>
</div>
</details>
<details class="ai-settings-group">
<summary>Recent slow and 504-risk requests</summary>
<div class="table-wrap">
<table class="table"><thead><tr><th>Time</th><th>Route / class</th><th>Reason / budget</th><th>Gate</th><th>Queue</th><th>Prompt eval</th><th>Generation</th><th>Tokens</th><th>Speed</th><th>Total</th><th>Risk</th></tr></thead><tbody>
<% slowRequestsPage.entries.forEach((entry) => { %><tr><td><%= entry.timestamp %></td><td><%= entry.route_used || "-" %> / <%= entry.route_class || "-" %></td><td><%= entry.reason_code || "-" %> / max <%= entry.max_output_tokens_used || "-" %></td><td><%= formatDuration(entry.gate_ms) %></td><td><%= formatDuration(entry.queue_ms) %></td><td><%= formatDuration(entry.prompt_eval_ms) %></td><td><%= formatDuration(entry.generation_ms) %></td><td><%= entry.prompt_tokens || 0 %> / <%= entry.generated_tokens || 0 %></td><td><%= entry.prompt_tps || 0 %> / <%= entry.generation_tps || 0 %> tok/s</td><td><%= formatDuration(entry.total_ms) %></td><td><%= entry.frontend_soft_timeout ? "UI waited" : entry.risk_504 ? "504 risk" : "Slow" %></td></tr><% }) %>
<% if (!slowRequestsPage.entries.length) { %><tr><td colspan="11">No requests over 30 seconds.</td></tr><% } %>
</tbody></table>
</div>
<div class="table-pagination">
<a class="button subtle <%= slowRequestsPage.page <= 1 ? 'disabled' : '' %>" href="?slow_page=<%= Math.max(1, slowRequestsPage.page - 1) %>#metrics">Previous slow requests</a>
<span class="table-page-label">Page <%= slowRequestsPage.page %> of <%= slowRequestsPage.pages %> (<%= slowRequestsPage.total %> slow requests)</span>
<a class="button subtle <%= slowRequestsPage.page >= slowRequestsPage.pages ? 'disabled' : '' %>" href="?slow_page=<%= Math.min(slowRequestsPage.pages, slowRequestsPage.page + 1) %>#metrics">Next slow requests</a>
</div>
</details>
<div class="table-wrap">
<table class="table"><thead><tr><th>Time</th><th>Kind</th><th>Status</th><th>Route</th><th>Confidence / reason</th><th>Role</th><th>Generated / final / delivered</th><th>Duration</th></tr></thead><tbody>
<% history.forEach((entry) => { %><tr><td><%= entry.timestamp %></td><td><%= entry.kind %></td><td><%= entry.status %></td><td><%= entry.route_used || "-" %></td><td><%= entry.confidence ?? entry.gate_confidence ?? "-" %> / <%= entry.reason_code || entry.gate_reason_code || "-" %></td><td><%= entry.role || "-" %></td><td><%= entry.internal_generated_length ?? "-" %> / <%= entry.final_reply_length ?? entry.original_final_length ?? "-" %> / <%= entry.delivered_length ?? "-" %></td><td><%= formatDuration(entry.duration_ms) %></td></tr><% }) %>
<% if (!history.length) { %><tr><td colspan="8">No requests recorded.</td></tr><% } %>
</tbody></table>
</div>
<div class="table-pagination">
<a class="button subtle <%= metricsPage.page <= 1 ? 'disabled' : '' %>" href="?metrics_page=<%= Math.max(1, metricsPage.page - 1) %>#metrics">Previous</a>
<span class="table-page-label">Page <%= metricsPage.page %> of <%= metricsPage.pages %> (<%= metricsPage.total %> entries)</span>
<a class="button subtle <%= metricsPage.page >= metricsPage.pages ? 'disabled' : '' %>" href="?metrics_page=<%= Math.min(metricsPage.pages, metricsPage.page + 1) %>#metrics">Next</a>
</div>
</section>
<section class="ai-band" id="logs">
<div class="ai-section-heading"><div><h2>Runtime logs</h2><p>Open a tail view without loading entire large files.</p></div></div>
<div class="table-wrap">
<table class="table">
<thead><tr><th>Filename</th><th>Size</th><th>Modified</th><th>Actions</th></tr></thead>
<tbody>
<% logFiles.forEach((file) => { %>
<tr>
<td><%= file.name %></td>
<td><%= formatBytes(file.size) %></td>
<td><%= formatDate(file.modified_at) %></td>
<td class="ai-table-actions">
<a class="button subtle" href="/plugins/lumi_ai/logs/<%= encodeURIComponent(file.name) %>">View</a>
<a class="button subtle" href="/plugins/lumi_ai/logs/<%= encodeURIComponent(file.name) %>/download">Download</a>
<form method="post" action="/plugins/lumi_ai/logs/<%= encodeURIComponent(file.name) %>/delete" data-confirm-title="Delete AI runtime log" data-confirm-text="Delete <%= file.name %>?" data-confirm-label="Delete log"><button class="button danger" type="submit">Delete</button></form>
</td>
</tr>
<% }) %>
<% if (!logFiles.length) { %><tr><td colspan="4">No runtime logs found.</td></tr><% } %>
</tbody>
</table>
</div>
<div class="table-pagination">
<a class="button subtle <%= logPage.page <= 1 ? 'disabled' : '' %>" href="?logs_page=<%= Math.max(1, logPage.page - 1) %>#logs">Previous</a>
<span class="table-page-label">Page <%= logPage.page %> of <%= logPage.pages %> (<%= logPage.total %> logs)</span>
<a class="button subtle <%= logPage.page >= logPage.pages ? 'disabled' : '' %>" href="?logs_page=<%= Math.min(logPage.pages, logPage.page + 1) %>#logs">Next</a>
</div>
</section>
<section class="ai-band">
<div class="ai-section-heading"><div><h2>Privacy and troubleshooting</h2><p>Local inference remains on this host.</p></div></div>
<div class="callout">
Models are downloaded from pinned Hugging Face revisions. The managed runtime is downloaded from the official llama.cpp release and verified by SHA-256. No cloud inference is used. Prompt and response logging are off by default.
</div>
<p class="hint">If startup fails, confirm that the runtime and selected model show as installed, the plugin directory is writable, and enough RAM and disk are available. Runtime logs are stored under <code>plugins/lumi_ai/data/logs/</code>.</p>
</section>
<%- include("tool-modal") %>
<script src="/plugins/lumi_ai/assets/settings.js?v=<%= assetVersion %>" defer></script>
<script src="/plugins/lumi_ai/assets/tool-manager.js?v=<%= assetVersion %>" defer></script>
<%- include("../../../src/web/views/partials/layout-bottom") %>