AUTARCH v1.9 — remote monitoring, SSH manager, daemon, vault, cleanup

- Add Remote Monitoring Station with PIAP device profile system
- Add SSH/SSHD manager with fail2ban integration
- Add privileged daemon architecture for safe root operations
- Add encrypted vault, HAL memory, HAL auto-analyst
- Add network security suite, module creator, codex training
- Add start.sh launcher script and GTK3 desktop launcher
- Remove Output/ build artifacts, installer files, loose docs
- Update .gitignore for runtime data and build artifacts
- Update README for v1.9 with new launch method, screenshots, and features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
SsSnake
2026-03-24 06:59:06 -07:00
parent 1092689f45
commit da53899f66
382 changed files with 15277 additions and 493964 deletions

View File

@@ -22,7 +22,7 @@
</div>
<p style="font-size:0.8rem;color:var(--text-secondary);margin-top:0.5rem">
Configured backend: <strong style="color:var(--accent)">{{ llm_backend }}</strong>
— select a tab, fill in settings, and click <em>Save &amp; Activate</em>, then <em>Load Model</em> to initialise.
— select a tab, fill in settings, and click <em>Save &amp; Activate</em>, then <em>Initialize LLM</em> to initialise.
</p>
<!-- Load / Status bar -->
@@ -32,10 +32,10 @@
<div id="llm-status-dot" style="width:10px;height:10px;border-radius:50%;
background:var(--text-muted);flex-shrink:0" title="Not loaded"></div>
<span id="llm-status-text" style="font-size:0.83rem;color:var(--text-secondary);flex:1">
Not loaded — click <strong>Load Model</strong> to initialise the current backend.
Not loaded — click <strong>Initialize LLM</strong> to initialise the current backend.
</span>
<button id="btn-llm-load" class="btn btn-primary btn-sm" onclick="loadLLM()">
Load Model
Initialize LLM
</button>
<button class="btn btn-sm" onclick="debugOpen()"
title="Open debug console to see detailed load output">
@@ -367,62 +367,71 @@
<div class="section">
<h2>Claude API</h2>
<p style="font-size:0.82rem;color:var(--text-secondary)">
Requires an <a href="https://console.anthropic.com" target="_blank" rel="noopener">Anthropic account</a>.
Get your API key from the console.
Requires an <a href="https://console.anthropic.com" target="_blank" rel="noopener">Anthropic API</a> key.
AUTARCH calls Claude directly for chat, agent, and analysis tasks.
</p>
<form method="POST" action="{{ url_for('settings.update_llm') }}" class="settings-form">
<input type="hidden" name="backend" value="claude">
<div class="form-group">
<label for="claude-key">API Key</label>
<input type="password" id="claude-key" name="api_key" value="{{ claude.api_key }}" placeholder="sk-ant-api03-...">
<small>Stored in autarch_settings.conf — keep it safe.</small>
</div>
<div class="form-group">
<label for="claude-key">API Key</label>
<input type="password" id="claude-key" value="{{ claude.api_key }}" placeholder="sk-ant-api03-...">
<small>Stored in autarch_settings.conf — keep it safe.</small>
</div>
<div class="form-group">
<label for="claude-model">Model</label>
<select id="claude-model" name="model">
<optgroup label="Claude 4.6">
<option value="claude-opus-4-6" {% if claude.model == 'claude-opus-4-6' %}selected{% endif %}>claude-opus-4-6 (most capable)</option>
<option value="claude-sonnet-4-6" {% if claude.model == 'claude-sonnet-4-6' %}selected{% endif %}>claude-sonnet-4-6 (balanced)</option>
</optgroup>
<optgroup label="Claude 4.5">
<option value="claude-opus-4-5" {% if claude.model == 'claude-opus-4-5' %}selected{% endif %}>claude-opus-4-5</option>
<option value="claude-sonnet-4-5" {% if claude.model == 'claude-sonnet-4-5' %}selected{% endif %}>claude-sonnet-4-5</option>
<option value="claude-haiku-4-5-20251001" {% if claude.model == 'claude-haiku-4-5-20251001' %}selected{% endif %}>claude-haiku-4-5 (fastest)</option>
</optgroup>
<optgroup label="Claude 3.5 / 3">
<option value="claude-3-5-sonnet-20241022" {% if claude.model == 'claude-3-5-sonnet-20241022' %}selected{% endif %}>claude-3-5-sonnet-20241022</option>
<option value="claude-3-5-haiku-20241022" {% if claude.model == 'claude-3-5-haiku-20241022' %}selected{% endif %}>claude-3-5-haiku-20241022</option>
<option value="claude-3-opus-20240229" {% if claude.model == 'claude-3-opus-20240229' %}selected{% endif %}>claude-3-opus-20240229</option>
</optgroup>
<div class="form-group">
<label for="claude-model">Model</label>
<div style="display:flex;gap:0.5rem;align-items:center">
<select id="claude-model" style="flex:1">
<option value="{{ claude.model }}" selected>{{ claude.model }}</option>
</select>
<button class="btn btn-sm" onclick="claudeFetchModels()" id="btn-claude-refresh" title="Fetch available models from API">Refresh</button>
</div>
<small id="claude-model-hint">Click <strong>Refresh</strong> to fetch available models from the API.</small>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem 1rem">
<div class="form-group">
<label for="claude-max-tok">Max Tokens</label>
<input type="number" id="claude-max-tok" name="max_tokens" value="{{ claude.max_tokens }}" min="1" max="200000">
</div>
<div class="form-group">
<label for="claude-temp">Temperature</label>
<input type="number" id="claude-temp" name="temperature" value="{{ claude.temperature }}" step="0.05" min="0" max="1">
<small>01. Claude default is 1.</small>
</div>
<div class="form-group">
<label for="claude-top-p">Top-P</label>
<input type="number" id="claude-top-p" name="top_p" value="{{ claude.get('top_p', 1.0) }}" step="0.05" min="0" max="1">
<small>Use with lower temp.</small>
</div>
<div class="form-group">
<label for="claude-top-k">Top-K</label>
<input type="number" id="claude-top-k" name="top_k" value="{{ claude.get('top_k', 0) }}" min="0">
<small>0 = disabled.</small>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem 1rem">
<div class="form-group">
<label for="claude-max-tok">Max Tokens</label>
<input type="number" id="claude-max-tok" value="{{ claude.max_tokens }}" min="1" max="200000">
</div>
<div class="form-group">
<label for="claude-temp">Temperature</label>
<input type="number" id="claude-temp" value="{{ claude.temperature }}" step="0.05" min="0" max="1">
<small>01. Claude default is 1.</small>
</div>
<div class="form-group">
<label for="claude-top-p">Top-P</label>
<input type="number" id="claude-top-p" value="{{ claude.get('top_p', 1.0) }}" step="0.05" min="0" max="1">
<small>Use with lower temp.</small>
</div>
<div class="form-group">
<label for="claude-top-k">Top-K</label>
<input type="number" id="claude-top-k" value="{{ claude.get('top_k', 0) }}" min="0">
<small>0 = disabled.</small>
</div>
</div>
<button type="submit" class="btn btn-primary">Save &amp; Activate Claude</button>
</form>
<!-- Action buttons + inline status -->
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin-top:0.5rem">
<button class="btn btn-primary" onclick="claudeActivate()" id="btn-claude-activate">
Activate Claude
</button>
<button class="btn btn-sm" onclick="claudeSave()" id="btn-claude-save">
Save Settings
</button>
<button class="btn btn-sm" onclick="claudeReload()" id="btn-claude-reload" title="Re-test API key and reconnect">
Reload
</button>
<div id="claude-status-dot" style="width:10px;height:10px;border-radius:50%;
background:{% if llm_backend == 'claude' %}var(--success, #34c759){% else %}var(--text-muted){% endif %};flex-shrink:0"></div>
<span id="claude-status-text" style="font-size:0.83rem;color:var(--text-secondary)">
{% if llm_backend == 'claude' %}Active — {{ claude.model }}{% else %}Not active{% endif %}
</span>
</div>
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.35rem">
<strong>Save</strong> stores settings + API key to encrypted vault.
<strong>Reload</strong> re-tests the connection without changing settings.
<strong>Activate</strong> saves + loads Claude as the active backend.
</p>
</div>
</div><!-- end tab-claude -->
@@ -605,9 +614,159 @@
</div>
</div><!-- end tab-huggingface -->
<!-- ══════════════════════════════════════════════════════════════════ -->
<!-- AGENTS SECTION -->
<!-- ══════════════════════════════════════════════════════════════════ -->
<div class="section" style="margin-top:2rem">
<h2>Agent Configuration</h2>
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">
Configure the AI agent backends used by Agent Hal and Autonomy.
Agents use the THOUGHT/ACTION/PARAMS loop to accomplish tasks with tools.
</p>
<!-- Agent sub-tabs -->
<div class="tab-bar" id="agent-tab-bar">
<button class="tab active" onclick="agentTab('local')">Local Agent</button>
<button class="tab" onclick="agentTab('claude')">Claude Agent</button>
<button class="tab" onclick="agentTab('openai')">OpenAI Agent</button>
</div>
<!-- ── Local Agent Sub-Tab ──────────────────────────────────────── -->
<div id="agent-tab-local" class="agent-tab-panel">
<div style="padding:1rem 0">
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
Uses the currently loaded LLM backend (configured above) for agent operations.
Best for offline or privacy-sensitive work.
</p>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
<div class="form-group">
<label for="agent-local-steps">Max Steps</label>
<input type="number" id="agent-local-steps" value="{{ agents.local_max_steps }}" min="1" max="100">
<small>Maximum tool-use steps per task.</small>
</div>
<div class="form-group">
<label for="agent-local-verbose">
<input type="checkbox" id="agent-local-verbose" {% if agents.local_verbose %}checked{% endif %}
style="margin-right:0.4rem">
Verbose Output
</label>
<small>Show step-by-step agent reasoning.</small>
</div>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
<button class="btn btn-primary btn-sm" onclick="agentSave('local')">Save Local Agent Settings</button>
<span id="agent-local-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
</div>
</div>
</div>
<!-- ── Claude Agent Sub-Tab ─────────────────────────────────────── -->
<div id="agent-tab-claude" class="agent-tab-panel hidden">
<div style="padding:1rem 0">
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
Uses the Anthropic Claude API with native tool use for agent operations.
Requires a Claude API key (configured in the Claude tab above).
Supports extended thinking and structured tool calls.
</p>
<div class="form-group">
<label for="agent-claude-model">Agent Model</label>
<div style="display:flex;gap:0.5rem;align-items:center">
<select id="agent-claude-model" style="flex:1">
<option value="{{ agents.claude_model }}" selected>{{ agents.claude_model }}</option>
</select>
<button class="btn btn-sm" onclick="agentClaudeFetchModels()" id="btn-agent-claude-refresh">Refresh</button>
</div>
<small id="agent-claude-model-hint">Click Refresh to fetch models from the API. Uses the API key from the Claude tab.</small>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
<div class="form-group">
<label for="agent-claude-tokens">Max Tokens</label>
<input type="number" id="agent-claude-tokens" value="{{ agents.claude_max_tokens }}" min="1" max="200000">
</div>
<div class="form-group">
<label for="agent-claude-steps">Max Steps</label>
<input type="number" id="agent-claude-steps" value="{{ agents.claude_max_steps }}" min="1" max="200">
</div>
<div class="form-group">
<label for="agent-claude-enabled">
<input type="checkbox" id="agent-claude-enabled" {% if agents.claude_enabled %}checked{% endif %}
style="margin-right:0.4rem">
Enable Claude Agent
</label>
<small>Allow agent tasks to use Claude.</small>
</div>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
<button class="btn btn-primary btn-sm" onclick="agentSave('claude')">Save Claude Agent Settings</button>
<span id="agent-claude-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
</div>
</div>
</div>
<!-- ── OpenAI Agent Sub-Tab ─────────────────────────────────────── -->
<div id="agent-tab-openai" class="agent-tab-panel hidden">
<div style="padding:1rem 0">
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
Uses the OpenAI API (or any compatible endpoint: Ollama, vLLM, LiteLLM) with function calling for agent operations.
Requires an API key configured in the OpenAI tab above.
</p>
<div class="form-group">
<label for="agent-openai-model">Agent Model</label>
<input type="text" id="agent-openai-model" value="{{ agents.openai_model }}" placeholder="gpt-4o"
list="agent-openai-model-list">
<datalist id="agent-openai-model-list">
<option value="gpt-4o">
<option value="gpt-4o-mini">
<option value="gpt-4.1">
<option value="gpt-4.1-mini">
<option value="gpt-4.1-nano">
<option value="o3">
<option value="o4-mini">
</datalist>
<small>Model ID. For local servers (Ollama, etc.) use the name you pulled.</small>
</div>
<div class="form-group">
<label for="agent-openai-base-url">Base URL</label>
<input type="text" id="agent-openai-base-url" value="{{ agents.openai_base_url }}" placeholder="https://api.openai.com/v1">
<small>Change for Ollama (<code>http://localhost:11434/v1</code>), vLLM, LiteLLM, etc.</small>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
<div class="form-group">
<label for="agent-openai-tokens">Max Tokens</label>
<input type="number" id="agent-openai-tokens" value="{{ agents.openai_max_tokens }}" min="1" max="200000">
</div>
<div class="form-group">
<label for="agent-openai-steps">Max Steps</label>
<input type="number" id="agent-openai-steps" value="{{ agents.openai_max_steps }}" min="1" max="200">
</div>
<div class="form-group">
<label for="agent-openai-enabled">
<input type="checkbox" id="agent-openai-enabled" {% if agents.openai_enabled %}checked{% endif %}
style="margin-right:0.4rem">
Enable OpenAI Agent
</label>
<small>Allow agent tasks to use OpenAI.</small>
</div>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
<button class="btn btn-primary btn-sm" onclick="agentSave('openai')">Save OpenAI Agent Settings</button>
<span id="agent-openai-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
</div>
</div>
</div>
<!-- Active agent backend indicator -->
<div style="margin-top:0.75rem;padding:0.5rem 0.75rem;border-radius:var(--radius);border:1px solid var(--border);
background:var(--bg-card);font-size:0.82rem;color:var(--text-secondary)">
Active agent backend: <strong id="agent-active-backend" style="color:var(--accent)">{{ agents.backend }}</strong>
— The agent will use this backend when processing tasks.
</div>
</div>
<style>
.hidden { display: none !important; }
.form-group small { display:block; font-size:0.72rem; color:var(--text-muted); margin-top:0.2rem; }
.agent-tab-panel.hidden { display: none !important; }
/* ── GPU Preset Cards ───────────────────────────────────────────────────── */
.gpu-presets {
@@ -667,7 +826,7 @@
</style>
<script>
// ── Load Model ────────────────────────────────────────────────────────────────
// ── Initialize LLM ────────────────────────────────────────────────────────────────
function loadLLM() {
var btn = document.getElementById('btn-llm-load');
var dot = document.getElementById('llm-status-dot');
@@ -683,7 +842,7 @@ function loadLLM() {
.then(function(r) { return r.json(); })
.then(function(d) {
btn.disabled = false;
btn.textContent = 'Load Model';
btn.textContent = 'Initialize LLM';
if (d.ok) {
dot.style.background = 'var(--success, #34c759)';
dot.title = 'Loaded';
@@ -700,7 +859,7 @@ function loadLLM() {
})
.catch(function(e) {
btn.disabled = false;
btn.textContent = 'Load Model';
btn.textContent = 'Initialize LLM';
dot.style.background = 'var(--danger, #ff3b30)';
dot.title = 'Error';
text.textContent = 'Request failed: ' + e.message;
@@ -866,5 +1025,277 @@ function hfVerifyToken() {
})
.catch(function(e) { btn.textContent = 'Verify Token'; btn.disabled = false; alert('Request failed: ' + e.message); });
}
// ── Claude: Activate (save + load in one click) ──────────────────────────────
function claudeSave() {
var btn = document.getElementById('btn-claude-save');
var text = document.getElementById('claude-status-text');
btn.disabled = true; btn.textContent = 'Saving…';
var fd = new FormData();
fd.append('backend', 'claude');
fd.append('api_key', document.getElementById('claude-key').value);
fd.append('model', document.getElementById('claude-model').value);
fd.append('max_tokens', document.getElementById('claude-max-tok').value);
fd.append('temperature', document.getElementById('claude-temp').value);
fetch('/settings/llm', {method: 'POST', body: fd, redirect: 'manual'})
.then(function() {
btn.disabled = false; btn.textContent = 'Save Settings';
text.innerHTML = '<span style="color:var(--success,#34c759)">&#x2713; Settings saved to encrypted vault</span>';
setTimeout(function() { text.textContent = ''; }, 3000);
})
.catch(function(e) {
btn.disabled = false; btn.textContent = 'Save Settings';
text.textContent = 'Save failed: ' + e.message;
});
}
function claudeReload() {
var btn = document.getElementById('btn-claude-reload');
var dot = document.getElementById('claude-status-dot');
var text = document.getElementById('claude-status-text');
btn.disabled = true; btn.textContent = 'Reloading…';
dot.style.background = '#f59e0b';
text.innerHTML = '<em>Re-testing Claude connection…</em>';
fetch('/settings/llm/load', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function(d) {
btn.disabled = false; btn.textContent = 'Reload';
if (d.ok) {
dot.style.background = 'var(--success, #34c759)';
text.innerHTML = '&#x2713; <strong style="color:var(--success,#34c759)">Connected</strong> — ' + escapeHtml(d.model_name);
} else {
dot.style.background = 'var(--danger, #ff3b30)';
text.innerHTML = '&#x2715; ' + escapeHtml(d.error || 'Connection failed');
}
})
.catch(function(e) {
btn.disabled = false; btn.textContent = 'Reload';
dot.style.background = 'var(--danger, #ff3b30)';
text.textContent = 'Failed: ' + e.message;
});
}
function claudeActivate() {
var btn = document.getElementById('btn-claude-activate');
var dot = document.getElementById('claude-status-dot');
var text = document.getElementById('claude-status-text');
btn.disabled = true;
btn.textContent = 'Saving…';
dot.style.background = '#f59e0b';
text.textContent = 'Saving settings…';
// Build form data from the Claude tab fields
var fd = new FormData();
fd.append('backend', 'claude');
fd.append('api_key', document.getElementById('claude-key').value);
fd.append('model', document.getElementById('claude-model').value);
fd.append('max_tokens', document.getElementById('claude-max-tok').value);
fd.append('temperature', document.getElementById('claude-temp').value);
// Step 1: Save settings via the existing update_llm route (no redirect — use fetch)
fetch('/settings/llm', {method: 'POST', body: fd, redirect: 'manual'})
.then(function() {
// Step 2: Now load the backend
btn.textContent = 'Loading…';
text.innerHTML = '<em>Initialising Claude — please wait…</em>';
return fetch('/settings/llm/load', {method: 'POST'});
})
.then(function(r) { return r.json(); })
.then(function(d) {
btn.disabled = false;
btn.textContent = 'Activate Claude';
if (d.ok) {
dot.style.background = 'var(--success, #34c759)';
text.innerHTML = '&#x2713; <strong style="color:var(--success,#34c759)">Claude active</strong> — '
+ escapeHtml(d.model_name);
// Also update the top-level status bar
var topDot = document.getElementById('llm-status-dot');
var topText = document.getElementById('llm-status-text');
if (topDot) { topDot.style.background = 'var(--success, #34c759)'; topDot.title = 'Loaded'; }
if (topText) { topText.innerHTML = '&#x2713; <strong style="color:var(--success,#34c759)">claude</strong> ready — ' + escapeHtml(d.model_name); }
} else {
dot.style.background = 'var(--danger, #ff3b30)';
text.innerHTML = '&#x2715; <strong style="color:var(--danger,#ff3b30)">Load failed:</strong> '
+ escapeHtml(d.error || 'Unknown error')
+ ' — <em>check Debug Log for details</em>';
}
})
.catch(function(e) {
btn.disabled = false;
btn.textContent = 'Activate Claude';
dot.style.background = 'var(--danger, #ff3b30)';
text.textContent = 'Request failed: ' + e.message;
});
}
// ── Agent tab switching ───────────────────────────────────────────────────────
function agentTab(name) {
// Hide all agent panels
var panels = document.querySelectorAll('.agent-tab-panel');
for (var i = 0; i < panels.length; i++) panels[i].classList.add('hidden');
// Deactivate all agent tabs
var tabs = document.querySelectorAll('#agent-tab-bar .tab');
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
// Show selected
var panel = document.getElementById('agent-tab-' + name);
if (panel) panel.classList.remove('hidden');
// Activate tab button (find by text match)
var labels = {local: 'Local Agent', claude: 'Claude Agent', openai: 'OpenAI Agent'};
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].textContent.trim() === labels[name]) tabs[i].classList.add('active');
}
}
// ── Agent save ───────────────────────────────────────────────────────────────
function agentSave(tab) {
var status = document.getElementById('agent-' + tab + '-status');
var data = {backend: tab};
if (tab === 'local') {
data.local_max_steps = parseInt(document.getElementById('agent-local-steps').value) || 20;
data.local_verbose = document.getElementById('agent-local-verbose').checked;
} else if (tab === 'claude') {
data.claude_enabled = document.getElementById('agent-claude-enabled').checked;
data.claude_model = document.getElementById('agent-claude-model').value;
data.claude_max_tokens = parseInt(document.getElementById('agent-claude-tokens').value) || 16384;
data.claude_max_steps = parseInt(document.getElementById('agent-claude-steps').value) || 30;
} else if (tab === 'openai') {
data.openai_enabled = document.getElementById('agent-openai-enabled').checked;
data.openai_model = document.getElementById('agent-openai-model').value;
data.openai_base_url = document.getElementById('agent-openai-base-url').value;
data.openai_max_tokens = parseInt(document.getElementById('agent-openai-tokens').value) || 16384;
data.openai_max_steps = parseInt(document.getElementById('agent-openai-steps').value) || 30;
}
status.textContent = 'Saving…';
fetch('/settings/agents/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
status.innerHTML = '<span style="color:var(--success,#34c759)">&#x2713; Saved</span>';
document.getElementById('agent-active-backend').textContent = tab;
} else {
status.innerHTML = '<span style="color:var(--danger,#ff3b30)">&#x2715; ' + escapeHtml(d.error || 'Error') + '</span>';
}
setTimeout(function() { status.textContent = ''; }, 4000);
})
.catch(function(e) {
status.textContent = 'Failed: ' + e.message;
});
}
// ── Agent Claude: Fetch models (reuses the same API endpoint) ────────────────
function agentClaudeFetchModels() {
var btn = document.getElementById('btn-agent-claude-refresh');
var sel = document.getElementById('agent-claude-model');
var hint = document.getElementById('agent-claude-model-hint');
var curVal = sel.value;
btn.disabled = true;
btn.textContent = 'Fetching…';
hint.textContent = 'Querying Anthropic API…';
fetch('/settings/llm/claude-models', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function(d) {
btn.disabled = false;
btn.textContent = 'Refresh';
if (!d.ok) {
hint.textContent = 'Error: ' + (d.error || 'Unknown');
return;
}
sel.innerHTML = '';
var models = d.models || [];
if (models.length === 0) {
hint.textContent = 'No models returned.';
var opt = document.createElement('option');
opt.value = curVal; opt.textContent = curVal; opt.selected = true;
sel.appendChild(opt);
return;
}
var foundCurrent = false;
for (var i = 0; i < models.length; i++) {
var m = models[i];
var opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name || m.id;
if (m.id === curVal) { opt.selected = true; foundCurrent = true; }
sel.appendChild(opt);
}
if (!foundCurrent && curVal) {
var opt = document.createElement('option');
opt.value = curVal; opt.textContent = curVal + ' (current)'; opt.selected = true;
sel.insertBefore(opt, sel.firstChild);
}
hint.textContent = models.length + ' models available.';
})
.catch(function(e) {
btn.disabled = false;
btn.textContent = 'Refresh';
hint.textContent = 'Failed: ' + e.message;
});
}
// ── Claude: Fetch models from API ────────────────────────────────────────────
function claudeFetchModels() {
var btn = document.getElementById('btn-claude-refresh');
var sel = document.getElementById('claude-model');
var hint = document.getElementById('claude-model-hint');
var curVal = sel.value;
btn.disabled = true;
btn.textContent = 'Fetching…';
hint.textContent = 'Querying Anthropic API…';
fetch('/settings/llm/claude-models', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function(d) {
btn.disabled = false;
btn.textContent = 'Refresh';
if (!d.ok) {
hint.textContent = 'Error: ' + (d.error || 'Unknown error');
return;
}
// Rebuild the dropdown with live models
sel.innerHTML = '';
var models = d.models || [];
if (models.length === 0) {
hint.textContent = 'No models returned — check API key permissions.';
var opt = document.createElement('option');
opt.value = curVal; opt.textContent = curVal; opt.selected = true;
sel.appendChild(opt);
return;
}
var foundCurrent = false;
for (var i = 0; i < models.length; i++) {
var m = models[i];
var opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name || m.id;
if (m.id === curVal) { opt.selected = true; foundCurrent = true; }
sel.appendChild(opt);
}
// Keep current value selected even if not in list
if (!foundCurrent && curVal) {
var opt = document.createElement('option');
opt.value = curVal; opt.textContent = curVal + ' (current)'; opt.selected = true;
sel.insertBefore(opt, sel.firstChild);
}
hint.textContent = models.length + ' models available.';
})
.catch(function(e) {
btn.disabled = false;
btn.textContent = 'Refresh';
hint.textContent = 'Request failed: ' + e.message;
});
}
</script>
{% endblock %}