feat: add claude-code LLM provider + update dashboard to fo-blog-v5

- client.ts: add claude-code provider routing BLOG_LLM_PROVIDER=claude-code
  to claude-bridge (flat-rate, no API billing via Claude Code subscription)
- checkHealth() now pings /health on claude-bridge for real availability check
- Default OLLAMA_LLM_MODEL changed from qwen2.5:14b to fo-blog-v5
- Dashboard: add claude-code card (EMPFOHLEN), rename fo-blog-v3 → fo-blog-v5
- loadBlogLLMStatus() handles all 3 providers: claude-code/anthropic/ollama
- Grid expanded from 3 to 4 columns to accommodate new card
- ecosystem.config.js + .env on Erik: OLLAMA_LLM_MODEL=fo-blog-v5 confirmed
This commit is contained in:
Rene Fichtmueller 2026-04-18 20:45:14 +02:00
parent 74b83de6e9
commit 62d97a783c
3 changed files with 140 additions and 30 deletions

View File

@ -3,6 +3,7 @@
Format: `{"d":"YYYY-MM-DD","t":"TYPE","m":"Description"}` Format: `{"d":"YYYY-MM-DD","t":"TYPE","m":"Description"}`
Types: FEAT · FIX · UI · DATA · AI · INFRA Types: FEAT · FIX · UI · DATA · AI · INFRA
{"d":"2026-04-18","t":"AI","m":"Blog LLM: claude-code provider implemented in packages/api/src/llm/client.ts — routes BLOG_LLM_PROVIDER=claude-code to claude-bridge (http://localhost:3250/api/generate) on Erik using Claude Code flat-rate subscription. No API billing. checkHealth() pings /health endpoint. Dashboard updated: added claude-code card (EMPFOHLEN, AKTIV), fo-blog-v3-qwen7b card replaced with fo-blog-v5, loadBlogLLMStatus() now handles claude-code provider with correct badge/border highlighting. ecosystem.config.js + .env updated: OLLAMA_LLM_MODEL=fo-blog-v5, BLOG_LLM_PROVIDER=claude-code confirmed active via pm2 env."}
{"d":"2026-04-18","t":"FIX","m":"Cloudflare Tunnel DNS mass-update: after deleting phantom eo-pulse tunnel and creating main-prod (90c22eb0), 31 context-x.org + 7 fichtmueller.org DNS records still pointed to the deleted 641c39a5 tunnel → 530 on all services. Bulk-patched via Cloudflare API: all records now point to main-prod. Created missing admin.magatama.fichtmueller.org CNAME. TIP cloudflared-tip.service restart policy changed to Restart=always (was on-failure, so clean exits caused permanent outage). peercortex.org remains 530 — DNS is in a separate inaccessible Cloudflare account (NS: fattouche/elisabeth.ns.cloudflare.com); needs manual login."} {"d":"2026-04-18","t":"FIX","m":"Cloudflare Tunnel DNS mass-update: after deleting phantom eo-pulse tunnel and creating main-prod (90c22eb0), 31 context-x.org + 7 fichtmueller.org DNS records still pointed to the deleted 641c39a5 tunnel → 530 on all services. Bulk-patched via Cloudflare API: all records now point to main-prod. Created missing admin.magatama.fichtmueller.org CNAME. TIP cloudflared-tip.service restart policy changed to Restart=always (was on-failure, so clean exits caused permanent outage). peercortex.org remains 530 — DNS is in a separate inaccessible Cloudflare account (NS: fattouche/elisabeth.ns.cloudflare.com); needs manual login."}
{"d":"2026-04-18","t":"DATA","m":"Image backfill: GBICS og:image + QSFPTEK backfill scripts run on Erik — 226 new images added (671 → 897 total, 17.5% → 23.4% coverage). OSFP form factor: 0 → 68 images. QSFPTEK og:image URL bug fixed (double-hostname prefix stripped). OSFP-DR8-800G manually set to GBICS-compatible image (cdn11.bigcommerce.com DR8 product photo)."} {"d":"2026-04-18","t":"DATA","m":"Image backfill: GBICS og:image + QSFPTEK backfill scripts run on Erik — 226 new images added (671 → 897 total, 17.5% → 23.4% coverage). OSFP form factor: 0 → 68 images. QSFPTEK og:image URL bug fixed (double-hostname prefix stripped). OSFP-DR8-800G manually set to GBICS-compatible image (cdn11.bigcommerce.com DR8 product photo)."}
{"d":"2026-04-18","t":"FIX","m":"FS.com scraper: all 247 prices written as €79 (wrong) — root cause: 'Gratis Versand ab 79 € (ohne MwSt.)' free-shipping banner appears on every FS.com product page. PRICE_QUALIFIED bodyText regex matched this banner text before reaching the actual product price. Fix: (1) DOM-based price extraction added to page.evaluate — targets [class*='price-value']/[class*='product-price'] etc., skipping elements inside shipping/banner/footer parents; (2) bodyText qualified patterns now check 200-char context for versand/shipping/gratis keywords and skip matches that appear in shipping context; (3) waitForSelector for price elements added before evaluate; (4) deleted 247 invalid €79 observations from DB."} {"d":"2026-04-18","t":"FIX","m":"FS.com scraper: all 247 prices written as €79 (wrong) — root cause: 'Gratis Versand ab 79 € (ohne MwSt.)' free-shipping banner appears on every FS.com product page. PRICE_QUALIFIED bodyText regex matched this banner text before reaching the actual product price. Fix: (1) DOM-based price extraction added to page.evaluate — targets [class*='price-value']/[class*='product-price'] etc., skipping elements inside shipping/banner/footer parents; (2) bodyText qualified patterns now check 200-char context for versand/shipping/gratis keywords and skip matches that appear in shipping context; (3) waitForSelector for price elements added before evaluate; (4) deleted 247 invalid €79 observations from DB."}

View File

@ -1,20 +1,23 @@
/** /**
* LLM client for blog generation supports Ollama (local) and Anthropic Claude (API). * LLM client for blog generation supports Ollama (local), Anthropic Claude (API),
* and Claude-Code (flat-rate via claude-bridge on Erik).
* *
* Provider selection: * Provider selection:
* BLOG_LLM_PROVIDER=claude-code Claude via claude-bridge (flat-rate, recommended)
* BLOG_LLM_PROVIDER=anthropic Claude Sonnet/Haiku via Anthropic API * BLOG_LLM_PROVIDER=anthropic Claude Sonnet/Haiku via Anthropic API
* BLOG_LLM_PROVIDER=ollama qwen2.5 on local Ollama (default) * BLOG_LLM_PROVIDER=ollama fine-tuned fo-blog-v5 on local Ollama (default)
* *
* Claude is strongly recommended for blog generation qwen2.5:14b cannot * Claude-code is preferred: uses Claude Code subscription (flat-rate), no API costs.
* follow complex multi-constraint prompts (mode collapse). * Ollama fo-blog-v5 is the fallback for offline/local usage.
*/ */
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const LLM_MODEL = process.env.OLLAMA_LLM_MODEL || "qwen2.5:14b"; const LLM_MODEL = process.env.OLLAMA_LLM_MODEL || "fo-blog-v5";
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ""; const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514"; const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
const BLOG_LLM_PROVIDER = process.env.BLOG_LLM_PROVIDER || "ollama"; const BLOG_LLM_PROVIDER = process.env.BLOG_LLM_PROVIDER || "ollama";
const CLAUDE_BRIDGE_URL = process.env.CLAUDE_BRIDGE_URL || "http://localhost:3250";
interface LlmResponse { interface LlmResponse {
text: string; text: string;
@ -222,6 +225,48 @@ async function generateOllama(
}); });
} }
// ═══════════════════════════════════════════════════════
// CLAUDE-CODE PROVIDER (claude-bridge — flat-rate via Claude Code subscription)
// ═══════════════════════════════════════════════════════
async function generateClaudeBridge(
systemPrompt: string,
userPrompt: string,
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
): Promise<LlmResponse> {
const startTime = Date.now();
// claude-bridge expects combined prompt — system + user joined with double newline
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: fullPrompt }),
signal: AbortSignal.timeout(options?.timeoutMs ?? 300000),
});
if (!resp.ok) {
const errText = await resp.text();
throw new Error(`Claude bridge failed: ${resp.status} ${errText.slice(0, 200)}`);
}
const data = await resp.json() as { success: boolean; content?: string; error?: string };
if (!data.success || !data.content) {
throw new Error(`Claude bridge returned empty response: ${JSON.stringify(data)}`);
}
const duration = Date.now() - startTime;
console.log(`[LLM] Claude-bridge: ${data.content.length} chars, ${duration}ms`);
return {
text: data.content,
model: "claude-code",
totalDuration: duration * 1_000_000, // ns for compat with Ollama callers
evalCount: Math.ceil(data.content.length / 4), // approx tokens
};
}
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
// PUBLIC API — auto-routes to configured provider // PUBLIC API — auto-routes to configured provider
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
@ -231,6 +276,9 @@ export async function generate(
userPrompt: string, userPrompt: string,
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number }, options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
): Promise<LlmResponse> { ): Promise<LlmResponse> {
if (BLOG_LLM_PROVIDER === "claude-code") {
return generateClaudeBridge(systemPrompt, userPrompt, options);
}
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) { if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
return generateClaude(systemPrompt, userPrompt, options); return generateClaude(systemPrompt, userPrompt, options);
} }
@ -281,6 +329,16 @@ export async function chat(
/** Check if configured LLM provider is available */ /** Check if configured LLM provider is available */
export async function checkHealth(): Promise<{ ok: boolean; model: string; provider: string; error?: string }> { export async function checkHealth(): Promise<{ ok: boolean; model: string; provider: string; error?: string }> {
if (BLOG_LLM_PROVIDER === "claude-code") {
try {
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/health`, { signal: AbortSignal.timeout(5000) });
if (!resp.ok) return { ok: false, model: "claude-code", provider: "claude-code", error: `HTTP ${resp.status}` };
return { ok: true, model: "claude-code", provider: "claude-code" };
} catch (err) {
return { ok: false, model: "claude-code", provider: "claude-code", error: (err as Error).message };
}
}
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) { if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
// Key presence check only — live API call causes 429 when pipeline is running // Key presence check only — live API call causes 429 when pipeline is running
return { ok: true, model: ANTHROPIC_MODEL, provider: "anthropic" }; return { ok: true, model: ANTHROPIC_MODEL, provider: "anthropic" };

View File

@ -1243,23 +1243,48 @@
</div> </div>
<!-- Model cards --> <!-- Model cards -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.85rem"> <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.85rem">
<!-- Claude (recommended) --> <!-- Claude-Code (active — flat-rate via claude-bridge) -->
<div id="blog-model-card-cc" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
<div>
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🤖 claude-code</div>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">claude-bridge / Erik</div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
<span style="font-size:0.62rem;padding:2px 6px;border-radius:3px;background:var(--accent);color:#fff;font-weight:700;white-space:nowrap">★ EMPFOHLEN</span>
<span id="blog-model-cc-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
</div>
</div>
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
<div><span style="color:var(--accent)">★★★★★</span> Blog-Qualität</div>
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)"></span> Geschwindigkeit</div>
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Kein Mode Collapse</div>
<div style="color:#1a7a3a;font-weight:500">✓ Flat-rate (kein API-Billing)</div>
<div style="color:#1a7a3a;font-weight:500">✓ Claude Code Subscription</div>
</div>
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=claude-code<br>CLAUDE_BRIDGE_URL=http://localhost:3250</code>
</div>
<div id="blog-model-cc-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
</div>
<!-- Claude API -->
<div id="blog-model-card-claude" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)"> <div id="blog-model-card-claude" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem"> <div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
<div> <div>
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🧠 claude-sonnet-4-6</div> <div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🧠 claude-sonnet-4-6</div>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Anthropic API</div> <div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Anthropic API</div>
</div> </div>
<span style="font-size:0.62rem;padding:2px 6px;border-radius:3px;background:var(--accent);color:#fff;font-weight:700;white-space:nowrap">★ EMPFOHLEN</span> <span id="blog-model-claude-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
</div> </div>
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem"> <div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
<div><span style="color:var(--accent)">★★★★★</span> Blog-Qualität</div> <div><span style="color:var(--accent)">★★★★★</span> Blog-Qualität</div>
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)"></span> Geschwindigkeit</div> <div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)"></span> Geschwindigkeit</div>
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Komplexe Multi-Constraint Prompts</div> <div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Komplexe Multi-Constraint Prompts</div>
<div style="color:#1a7a3a;font-weight:500">✓ Kein Mode Collapse</div> <div style="color:#1a7a3a;font-weight:500">✓ Kein Mode Collapse</div>
<div style="color:#1a7a3a;font-weight:500">✓ 4096 Token Output</div> <div style="color:#b45309;font-weight:500">⚠ API-Kosten pro Artikel</div>
</div> </div>
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem"> <div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=anthropic<br>ANTHROPIC_MODEL=claude-sonnet-4-6</code> <code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=anthropic<br>ANTHROPIC_MODEL=claude-sonnet-4-6</code>
@ -1267,11 +1292,11 @@
<div id="blog-model-claude-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div> <div id="blog-model-claude-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
</div> </div>
<!-- Fine-tuned local --> <!-- Fine-tuned local fo-blog-v5 -->
<div id="blog-model-card-fo" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)"> <div id="blog-model-card-fo" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem"> <div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
<div> <div>
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v3-qwen7b</div> <div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v5</div>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div> <div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div>
</div> </div>
<span id="blog-model-fo-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span> <span id="blog-model-fo-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
@ -1279,12 +1304,12 @@
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem"> <div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)"></span> Blog-Qualität</div> <div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)"></span> Blog-Qualität</div>
<div><span style="color:var(--accent)">★★★★★</span> Geschwindigkeit</div> <div><span style="color:var(--accent)">★★★★★</span> Geschwindigkeit</div>
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Fine-tuned auf TIP-Stil</div> <div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Fine-tuned auf TIP-Stil (v5)</div>
<div style="color:#1a7a3a;font-weight:500">✓ Lokal / kein API-Kosten</div> <div style="color:#1a7a3a;font-weight:500">✓ Lokal / keine API-Kosten</div>
<div style="color:#b45309;font-weight:500">⚠ Gelegentlicher Mode Collapse</div> <div style="color:#b45309;font-weight:500">⚠ Gelegentlicher Mode Collapse</div>
</div> </div>
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem"> <div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v3-qwen7b</code> <code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v5</code>
</div> </div>
<div id="blog-model-fo-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div> <div id="blog-model-fo-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
</div> </div>
@ -1316,7 +1341,7 @@
<div style="margin-top:0.85rem;padding:0.6rem 0.85rem;background:var(--surface2);border-radius:6px;font-size:0.72rem;color:var(--text)"> <div style="margin-top:0.85rem;padding:0.6rem 0.85rem;background:var(--surface2);border-radius:6px;font-size:0.72rem;color:var(--text)">
<strong>Modell wechseln:</strong> SSH → Erik → <strong>Modell wechseln:</strong> SSH → Erik →
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">nano /opt/tip/ecosystem.config.js</code> <code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">nano /opt/tip/ecosystem.config.js</code>
→ BLOG_LLM_PROVIDER + ANTHROPIC_API_KEY → → BLOG_LLM_PROVIDER + CLAUDE_BRIDGE_URL / ANTHROPIC_API_KEY →
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">pm2 restart tip-api --update-env</code> <code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">pm2 restart tip-api --update-env</code>
</div> </div>
</div><!-- end LLM panel --> </div><!-- end LLM panel -->
@ -4934,7 +4959,8 @@ async function loadBlogLLMStatus() {
} }
if (activeModel) activeModel.textContent = llm.model || '—'; if (activeModel) activeModel.textContent = llm.model || '—';
if (activeProvider) { if (activeProvider) {
activeProvider.textContent = llm.provider === 'anthropic' ? 'anthropic' : 'ollama'; var provLabel = llm.provider === 'claude-code' ? 'claude-code' : llm.provider === 'anthropic' ? 'anthropic' : 'ollama';
activeProvider.textContent = provLabel;
activeProvider.style.background = 'var(--accent)'; activeProvider.style.background = 'var(--accent)';
activeProvider.style.color = '#fff'; activeProvider.style.color = '#fff';
} }
@ -4943,35 +4969,60 @@ async function loadBlogLLMStatus() {
queueEl.textContent = q > 0 ? 'Queue: ' + q + ' Jobs' : 'Queue: idle'; queueEl.textContent = q > 0 ? 'Queue: ' + q + ' Jobs' : 'Queue: idle';
} }
// Mark active model card with accent border // Reset all card borders + active badges
var foActive = document.getElementById('blog-model-fo-active'); ['cc','claude','fo'].forEach(function(k) {
var foCard = document.getElementById('blog-model-card-fo'); var card = document.getElementById('blog-model-card-' + k);
var claudeCard = document.getElementById('blog-model-card-claude'); if (card) card.style.border = '2px solid var(--border)';
var badge2 = document.getElementById('blog-model-' + k + '-active');
if (badge2) badge2.style.display = 'none';
});
if (llm.provider === 'anthropic') { if (llm.provider === 'claude-code') {
var ccCard = document.getElementById('blog-model-card-cc');
if (ccCard) ccCard.style.border = '2px solid var(--accent)';
var ccActive = document.getElementById('blog-model-cc-active');
if (ccActive) ccActive.style.display = 'inline';
var ccSt = document.getElementById('blog-model-cc-status');
if (ccSt) {
ccSt.textContent = llm.ok ? '● Aktiv — claude-bridge erreichbar' : '⚠ claude-bridge nicht erreichbar: ' + (llm.error || '').slice(0, 60);
ccSt.style.color = llm.ok ? '#1a7a3a' : '#b45309';
ccSt.style.fontWeight = '600';
}
var clSt2 = document.getElementById('blog-model-claude-status');
if (clSt2) { clSt2.textContent = 'bereit (nicht aktiv)'; clSt2.style.color = 'var(--text-dim)'; clSt2.style.fontWeight = '400'; }
var foSt2 = document.getElementById('blog-model-fo-status');
if (foSt2) { foSt2.textContent = 'bereit (nicht aktiv)'; foSt2.style.color = 'var(--text-dim)'; foSt2.style.fontWeight = '400'; }
} else if (llm.provider === 'anthropic') {
var claudeCard = document.getElementById('blog-model-card-claude');
if (claudeCard) claudeCard.style.border = '2px solid var(--accent)'; if (claudeCard) claudeCard.style.border = '2px solid var(--accent)';
var claudeActive = document.getElementById('blog-model-claude-active');
if (claudeActive) claudeActive.style.display = 'inline';
var claudeStatusEl = document.getElementById('blog-model-claude-status'); var claudeStatusEl = document.getElementById('blog-model-claude-status');
if (claudeStatusEl) { if (claudeStatusEl) {
claudeStatusEl.textContent = '● Aktiv — API-Key konfiguriert'; claudeStatusEl.textContent = '● Aktiv — API-Key konfiguriert';
claudeStatusEl.style.color = '#1a7a3a'; claudeStatusEl.style.color = '#1a7a3a';
claudeStatusEl.style.fontWeight = '600'; claudeStatusEl.style.fontWeight = '600';
} }
var foSt = document.getElementById('blog-model-fo-status'); var ccSt2 = document.getElementById('blog-model-cc-status');
if (foSt) { foSt.textContent = 'bereit (nicht aktiv)'; foSt.style.color = 'var(--text-dim)'; } if (ccSt2) { ccSt2.textContent = 'bereit (nicht aktiv)'; ccSt2.style.color = 'var(--text-dim)'; ccSt2.style.fontWeight = '400'; }
var foSt3 = document.getElementById('blog-model-fo-status');
if (foSt3) { foSt3.textContent = 'bereit (nicht aktiv)'; foSt3.style.color = 'var(--text-dim)'; foSt3.style.fontWeight = '400'; }
} else { } else {
if (foActive) foActive.style.display = 'inline'; // ollama
var foCard = document.getElementById('blog-model-card-fo');
if (foCard) foCard.style.border = '2px solid var(--accent)'; if (foCard) foCard.style.border = '2px solid var(--accent)';
var foActive = document.getElementById('blog-model-fo-active');
if (foActive) foActive.style.display = 'inline';
var foStatusEl = document.getElementById('blog-model-fo-status'); var foStatusEl = document.getElementById('blog-model-fo-status');
if (foStatusEl) { if (foStatusEl) {
foStatusEl.textContent = llm.ok ? '● Aktiv — Ollama erreichbar' : '⚠ Ollama nicht erreichbar: ' + (llm.error || '').slice(0, 60); foStatusEl.textContent = llm.ok ? '● Aktiv — Ollama erreichbar' : '⚠ Ollama nicht erreichbar: ' + (llm.error || '').slice(0, 60);
foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309'; foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309';
foStatusEl.style.fontWeight = '600'; foStatusEl.style.fontWeight = '600';
} }
var clSt = document.getElementById('blog-model-claude-status'); var ccSt3 = document.getElementById('blog-model-cc-status');
if (clSt) { if (ccSt3) { ccSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=claude-code setzen'; ccSt3.style.color = 'var(--text-dim)'; ccSt3.style.fontWeight = '400'; }
clSt.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY setzen'; var clSt3 = document.getElementById('blog-model-claude-status');
clSt.style.color = 'var(--text-dim)'; if (clSt3) { clSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + API-Key setzen'; clSt3.style.color = 'var(--text-dim)'; clSt3.style.fontWeight = '400'; }
}
} }
} catch(e) { } catch(e) {
var b = document.getElementById('blog-llm-status-badge'); var b = document.getElementById('blog-llm-status-badge');