@ -1243,23 +1243,48 @@
< / div >
<!-- 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 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-sonnet-4-6< / div >
< div style = "font-size:0.68rem;color:var(--text-dim);margin-top:2px" > Anthropic API< / 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 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" > ✓ Komplexe Multi-Constraint Prompts< / 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 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 >
@ -1267,11 +1292,11 @@
< div id = "blog-model-claude-status" style = "font-size:0.7rem;color:var(--text-dim)" > checking…< / 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 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)" > 🎯 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 >
< 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 > < 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 style = "margin-top:0.35rem;color:#1a7a3a;font-weight:500" > ✓ Fine-tuned auf TIP-Stil< / div >
< div style = "color:#1a7a3a;font-weight:500" > ✓ Lokal / kein API-Kosten< / 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 / keine API-Kosten< / div >
< div style = "color:#b45309;font-weight:500" > ⚠ Gelegentlicher Mode Collapse< / 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=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 id = "blog-model-fo-status" style = "font-size:0.7rem;color:var(--text-dim)" > checking…< / 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)" >
< 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 >
→ 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 >
< / div >
< / div > <!-- end LLM panel -->
@ -3474,12 +3499,91 @@ async function openTxDetail(id) {
directPrices.forEach(function(p) { h += renderPriceRow(p); });
if (comparPrices.length > 0) {
h += '< div style = "font-size:0.7rem;color:#888;margin:0.5rem 0 0.25rem;padding-top:0.4rem;border-top:1px solid var(--border)" > Vergleichbare Produkte anderer Hersteller (gleiche Spezifikation)< / div > ';
comparPrices.forEach(function(p) { h += renderPriceRow(p); });
}
h += '< / div > ';
// Comparable products → Side-by-Side spec comparison cards
if (comparPrices.length > 0) {
h += '< div class = "panel-section" style = "margin-top:0.8rem" > Vergleichbare Wettbewerber-Produkte< / div > ';
h += '< div style = "font-size:0.72rem;color:#888;margin-bottom:0.5rem" > Gleiche Spezifikationsklasse — andere Part Number< / div > ';
comparPrices.forEach(function(p) {
// Calculate price delta (EUR-normalized)
var myEur = null;
var refPrice = directPrices.length > 0 ? directPrices[0] : null;
if (refPrice) {
var ra = parseFloat(refPrice.price), rc = (refPrice.currency||'USD').toUpperCase();
myEur = rc === 'EUR' ? ra : rc === 'USD' ? ra * 0.92 : ra;
}
var compEur = null;
var ca = parseFloat(p.price), cc = (p.currency||'USD').toUpperCase();
compEur = cc === 'EUR' ? ca : cc === 'USD' ? ca * 0.92 : ca;
var savBadge = '';
if (myEur & & compEur & & myEur > 0 & & compEur > 0) {
var diff = myEur - compEur;
var pct = Math.round(Math.abs(diff) / myEur * 100);
if (diff > 0) {
savBadge = '< span style = "background:rgba(22,163,74,0.15);color:#16a34a;font-size:0.72rem;font-weight:700;padding:2px 7px;border-radius:4px;border:1px solid rgba(22,163,74,0.35)" > '
+ '− ' + pct + '% günstiger< / span > ';
} else if (diff < 0 ) {
savBadge = '< span style = "background:rgba(220,38,38,0.1);color:#dc2626;font-size:0.72rem;font-weight:700;padding:2px 7px;border-radius:4px;border:1px solid rgba(220,38,38,0.25)" > '
+ '+' + pct + '% teurer< / span > ';
}
}
// Spec comparison helper — highlight match/mismatch
function specRow(label, myVal, compVal) {
var match = myVal & & compVal & & String(myVal).toLowerCase() === String(compVal).toLowerCase();
var compColor = !myVal || !compVal ? '#aaa' : match ? '#4ade80' : '#fb923c';
return '< tr > < td style = "color:#888;font-size:0.7rem;padding:2px 6px 2px 0;white-space:nowrap" > ' + label + '< / td > '
+ '< td style = "color:#ccc;font-size:0.7rem;padding:2px 8px 2px 0" > ' + esc(myVal || '—') + '< / td > '
+ '< td style = "color:' + compColor + ';font-size:0.7rem;padding:2px 0" > ' + esc(compVal || '—') + '< / td > < / tr > ';
}
var mySpeed = t.speed_gbps >= 1000 ? (t.speed_gbps / 1000).toFixed(1).replace('.0','') + 'T' : t.speed_gbps + 'G';
var compSpeed = p.comp_speed_gbps ? (p.comp_speed_gbps >= 1000 ? (p.comp_speed_gbps/1000).toFixed(1).replace('.0','')+'T' : p.comp_speed_gbps+'G') : null;
h += '< div style = "border:1px solid var(--border);border-radius:8px;margin-bottom:0.6rem;overflow:hidden" > ';
// Header: vendor + part + price + savings badge
h += '< div style = "display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.75rem;background:rgba(255,255,255,0.03);border-bottom:1px solid var(--border)" > ';
h += '< div > ';
h += '< span style = "font-size:0.78rem;font-weight:700;color:var(--accent)" > ' + esc(p.vendor_name) + '< / span > ';
h += '< span style = "font-size:0.7rem;color:#888;margin-left:0.5rem" > ' + esc(p.comparable_part || '—') + '< / span > ';
h += '< / div > ';
h += '< div style = "display:flex;align-items:center;gap:0.4rem" > ';
var priceDisplayEur = compEur ? ('EUR\u00a0' + compEur.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})) : '';
h += '< span style = "font-size:0.82rem;font-weight:700;color:var(--text)" > ' + priceDisplayEur + '< / span > ';
if (p.url) h += '< a href = "' + esc(p.url) + '" target = "_blank" rel = "noopener" style = "color:var(--accent);font-size:0.7rem;text-decoration:none" > ↗< / a > ';
h += '< / div > ';
h += '< / div > ';
// Savings badge row
if (savBadge) {
h += '< div style = "padding:0.3rem 0.75rem;background:rgba(255,255,255,0.02);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:0.5rem" > ';
h += savBadge;
h += '< span style = "font-size:0.68rem;color:#666" > vs. Flexoptix Listenpreis< / span > ';
h += '< / div > ';
}
// Spec comparison table: Flexoptix (links) vs. Wettbewerber (rechts)
h += '< div style = "padding:0.5rem 0.75rem" > ';
h += '< table style = "width:100%;border-collapse:collapse" > ';
h += '< thead > < tr > ';
h += '< th style = "font-size:0.67rem;color:#555;text-align:left;padding-bottom:4px;padding-right:8px" > < / th > ';
h += '< th style = "font-size:0.67rem;color:#888;text-align:left;padding-bottom:4px;padding-right:8px" > Flexoptix< / th > ';
h += '< th style = "font-size:0.67rem;color:#888;text-align:left;padding-bottom:4px" > ' + esc(p.vendor_name) + '< / th > ';
h += '< / tr > < / thead > < tbody > ';
h += specRow('Form Factor', t.form_factor, p.comp_form_factor);
h += specRow('Speed', mySpeed, compSpeed);
h += specRow('Reach', t.reach_label || (t.reach_meters ? t.reach_meters + 'm' : null), p.comp_reach_label || (p.comp_reach_meters ? p.comp_reach_meters + 'm' : null));
h += specRow('Fiber', t.fiber_type, p.comp_fiber_type);
if (t.wavelengths || p.comp_wavelengths) h += specRow('Wavelengths', t.wavelengths, p.comp_wavelengths);
h += '< / tbody > < / table > ';
h += '< div style = "font-size:0.65rem;color:#555;margin-top:0.35rem" > 🕐 Stand: ' + fmtDate(p.observed_at) + (p.is_verified ? ' · < span style = "color:#2d6a4f" > ✓ Verified< / span > ' : '') + '< / div > ';
h += '< / div > ';
h += '< / div > '; // card end
});
}
}
// No competitor prices → show "Kein Markt" info block with last scan date
if (!cVer & & t.last_competitor_scan) {
@ -4934,7 +5038,8 @@ async function loadBlogLLMStatus() {
}
if (activeModel) activeModel.textContent = llm.model || '—';
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.color = '#fff';
}
@ -4943,35 +5048,60 @@ async function loadBlogLLMStatus() {
queueEl.textContent = q > 0 ? 'Queue: ' + q + ' Jobs' : 'Queue: idle';
}
// Mark active model card with accent border
var foActive = document.getElementById('blog-model-fo-active');
var foCard = document.getElementById('blog-model-card-fo');
var claudeCard = document.getElementById('blog-model-card-claude');
// Reset all card borders + active badges
['cc','claude','fo'].forEach(function(k) {
var card = document.getElementById('blog-model-card-' + k);
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)';
var claudeActive = document.getElementById('blog-model-claude-active');
if (claudeActive) claudeActive.style.display = 'inline';
var claudeStatusEl = document.getElementById('blog-model-claude-status');
if (claudeStatusEl) {
claudeStatusEl.textContent = '● Aktiv — API-Key konfiguriert';
claudeStatusEl.style.color = '#1a7a3a';
claudeStatusEl.style.fontWeight = '600';
}
var foSt = document.getElementById('blog-model-fo-status');
if (foSt) { foSt.textContent = 'bereit (nicht aktiv)'; foSt.style.color = 'var(--text-dim)'; }
var ccSt2 = document.getElementById('blog-model-cc-status');
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 {
if (foActive) foActive.style.display = 'inline';
// ollama
var foCard = document.getElementById('blog-model-card-fo');
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');
if (foStatusEl) {
foStatusEl.textContent = llm.ok ? '● Aktiv — Ollama erreichbar' : '⚠ Ollama nicht erreichbar: ' + (llm.error || '').slice(0, 60);
foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309';
foStatusEl.style.fontWeight = '600';
}
var clSt = document.getElementById('blog-model-claude-status');
if (clSt) {
clSt.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY setzen';
clSt.style.color = 'var(--text-dim)';
}
var ccSt3 = document.getElementById('blog-model-cc-status');
if (ccSt3) { ccSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=claude-code setzen'; ccSt3.style.color = 'var(--text-dim)'; ccSt3.style.fontWeight = '400'; }
var clSt3 = document.getElementById('blog-model-claude-status');
if (clSt3) { clSt3.textContent = 'bereit — BLOG_LLM_PROVIDER=anthropic + API-Key setzen'; clSt3.style.color = 'var(--text-dim)'; clSt3.style.fontWeight = '400'; }
}
} catch(e) {
var b = document.getElementById('blog-llm-status-badge');