fix: remove DEMO labels from real stock data; fix switch Flexoptix suggestions; enrich Hot Topics LLM context
Stock dashboard (index.html): - Replace all [DEMO]/demo badges on warehouse data with "FS.com" source labels (data was always real scraper data, never demo in the DB) - Update subtitle: "Scraper-Lagermengen: DEMO DATA" → "Wettbewerber-Marktdaten" - "Recently Restocked" badge: DEMO DATA → SCRAPER DATA Switch detail (queries.ts): - Fix getFlexoptixSuggestions: wavelength_nm → wavelength_tx_nm, price_verified_usd → street_price_usd (column mismatch with live schema) - DS5000 and other OSFP switches now show all 62 Flexoptix OSFP transceivers with direct shop links in the detail modal Hot Topics (hot-topics.ts): - NOG Talks + News Article clusters now fetch summary/mentioned_vendors/ mentioned_products/mentioned_standards from news_articles table - description field builds bullet-point list per article with summaries, key vendors/standards (vs. 3 bare titles joined with "|" before) - buildTopicBriefing() rewritten as structured LLM document with sections: Market Signals (bullets), Recommended Angle, Market Context (buy signal, technologies, impact horizon), Writing Instructions (600-900 words, actionable, opinionated, no generic summaries)
This commit is contained in:
parent
0d7a92e749
commit
de179c4c7c
@ -341,9 +341,9 @@ export async function getFlexoptixSuggestions(switchId: string) {
|
|||||||
)
|
)
|
||||||
SELECT t.id, t.slug, t.part_number, t.standard_name, t.form_factor,
|
SELECT t.id, t.slug, t.part_number, t.standard_name, t.form_factor,
|
||||||
t.speed, t.speed_gbps, t.reach_meters, t.reach_label,
|
t.speed, t.speed_gbps, t.reach_meters, t.reach_label,
|
||||||
t.fiber_type, t.wavelength_nm, t.market_status,
|
t.fiber_type, t.wavelength_tx_nm AS wavelength_nm, t.market_status,
|
||||||
t.product_page_url, t.image_url,
|
t.product_page_url, t.image_url,
|
||||||
t.price_verified_eur, t.price_verified_at, t.price_verified_usd,
|
t.price_verified_eur, t.price_verified_at, t.street_price_usd AS price_verified_usd,
|
||||||
v.name AS vendor_name, v.website AS vendor_website,
|
v.name AS vendor_name, v.website AS vendor_website,
|
||||||
COALESCE(t.price_verified_eur,
|
COALESCE(t.price_verified_eur,
|
||||||
(SELECT po.price FROM price_observations po
|
(SELECT po.price FROM price_observations po
|
||||||
|
|||||||
@ -172,7 +172,8 @@ hotTopicsRouter.get("/", async (req, res) => {
|
|||||||
|
|
||||||
// ═══ SOURCE 3c: NOG Conference Talks — scraped from NOG agendas ═══
|
// ═══ SOURCE 3c: NOG Conference Talks — scraped from NOG agendas ═══
|
||||||
const nogTalks = await pool.query(`
|
const nogTalks = await pool.query(`
|
||||||
SELECT title, source, source_url, published_at, relevance_score
|
SELECT title, source, source_url, published_at, relevance_score,
|
||||||
|
summary, mentioned_vendors, mentioned_products, mentioned_standards
|
||||||
FROM news_articles
|
FROM news_articles
|
||||||
WHERE source LIKE 'NOG Talks:%'
|
WHERE source LIKE 'NOG Talks:%'
|
||||||
AND relevance_score > 0.4
|
AND relevance_score > 0.4
|
||||||
@ -191,11 +192,17 @@ hotTopicsRouter.get("/", async (req, res) => {
|
|||||||
}
|
}
|
||||||
for (const [event, talks] of Object.entries(nogByEvent)) {
|
for (const [event, talks] of Object.entries(nogByEvent)) {
|
||||||
const topTalk = (talks as NogRow[])[0];
|
const topTalk = (talks as NogRow[])[0];
|
||||||
|
const talkBullets = (talks as NogRow[]).slice(0, 5).map(t => {
|
||||||
|
const vendors = Array.isArray(t.mentioned_vendors) ? (t.mentioned_vendors as string[]).slice(0, 3).join(", ") : "";
|
||||||
|
const products = Array.isArray(t.mentioned_products) ? (t.mentioned_products as string[]).slice(0, 3).join(", ") : "";
|
||||||
|
const extra = [vendors, products].filter(Boolean).join(" / ");
|
||||||
|
return `• ${t.title}${extra ? ` (${extra})` : ""}${t.summary ? ` — ${String(t.summary).slice(0, 120)}` : ""}`;
|
||||||
|
}).join("\n");
|
||||||
topics.push({
|
topics.push({
|
||||||
title: talks.length === 1
|
title: talks.length === 1
|
||||||
? `[${event}] ${topTalk.title}`
|
? `[${event}] ${topTalk.title}`
|
||||||
: `${event}: ${talks.length} optics-relevant talks`,
|
: `${event}: ${talks.length} optics-relevant talks`,
|
||||||
description: (talks as NogRow[]).map(t => t.title).slice(0, 3).join(" | "),
|
description: talkBullets || (talks as NogRow[]).map(t => t.title).slice(0, 3).join(" | "),
|
||||||
blog_type: "technology_deep_dive",
|
blog_type: "technology_deep_dive",
|
||||||
urgency: "hot",
|
urgency: "hot",
|
||||||
source: event,
|
source: event,
|
||||||
@ -209,7 +216,8 @@ hotTopicsRouter.get("/", async (req, res) => {
|
|||||||
// ═══ SOURCE 4: News Articles — Recent Industry News ═══
|
// ═══ SOURCE 4: News Articles — Recent Industry News ═══
|
||||||
const recentNews = await pool.query(`
|
const recentNews = await pool.query(`
|
||||||
SELECT title, source, source_url, category, published_at,
|
SELECT title, source, source_url, category, published_at,
|
||||||
COALESCE(relevance_score, 5) AS relevance
|
COALESCE(relevance_score, 5) AS relevance,
|
||||||
|
summary, mentioned_vendors, mentioned_products, mentioned_standards, tags
|
||||||
FROM news_articles
|
FROM news_articles
|
||||||
WHERE source NOT LIKE 'NOG Talks:%'
|
WHERE source NOT LIKE 'NOG Talks:%'
|
||||||
AND published_at > NOW() - INTERVAL '14 days'
|
AND published_at > NOW() - INTERVAL '14 days'
|
||||||
@ -228,14 +236,22 @@ hotTopicsRouter.get("/", async (req, res) => {
|
|||||||
|
|
||||||
for (const [theme, articles] of Object.entries(newsThemes)) {
|
for (const [theme, articles] of Object.entries(newsThemes)) {
|
||||||
if (articles.length >= 1) {
|
if (articles.length >= 1) {
|
||||||
|
// Build rich description with article summaries, vendors, standards mentioned
|
||||||
|
const articleBullets = (articles as NewsRow[]).slice(0, 5).map(a => {
|
||||||
|
const vendors = Array.isArray(a.mentioned_vendors) ? (a.mentioned_vendors as string[]).slice(0, 3).join(", ") : "";
|
||||||
|
const stds = Array.isArray(a.mentioned_standards) ? (a.mentioned_standards as string[]).slice(0, 2).join(", ") : "";
|
||||||
|
const meta = [vendors, stds].filter(Boolean).join(" / ");
|
||||||
|
return `• ${a.title}${meta ? ` [${meta}]` : ""}${a.summary ? ` — ${String(a.summary).slice(0, 150)}` : ""}`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
topics.push({
|
topics.push({
|
||||||
title: `${theme}: ${articles.length} recent articles`,
|
title: `${theme}: ${articles.length} recent articles`,
|
||||||
description: articles.map(a => a.title).slice(0, 3).join(" | "),
|
description: articleBullets || articles.map(a => a.title).slice(0, 3).join(" | "),
|
||||||
blog_type: "technology_deep_dive",
|
blog_type: "technology_deep_dive",
|
||||||
urgency: "trending",
|
urgency: "trending",
|
||||||
source: articles.map(a => a.source).filter(Boolean).slice(0, 2).join(", ") || "Trade Press",
|
source: articles.map(a => a.source).filter(Boolean).slice(0, 2).join(", ") || "Trade Press",
|
||||||
source_type: "trade_press",
|
source_type: "trade_press",
|
||||||
data_context: { articles: articles.slice(0, 3) },
|
data_context: { articles: articles.slice(0, 5) },
|
||||||
suggested_angle: `${theme}: What the latest announcements actually mean for network operators`,
|
suggested_angle: `${theme}: What the latest announcements actually mean for network operators`,
|
||||||
date: articles[0]?.published_at ? new Date(articles[0].published_at).toISOString() : undefined,
|
date: articles[0]?.published_at ? new Date(articles[0].published_at).toISOString() : undefined,
|
||||||
});
|
});
|
||||||
@ -407,22 +423,55 @@ function compactDataContext(data: Record<string, unknown> | undefined): string {
|
|||||||
|
|
||||||
function buildTopicBriefing(topic: HotTopic): string {
|
function buildTopicBriefing(topic: HotTopic): string {
|
||||||
const lines = [
|
const lines = [
|
||||||
`Topic: ${topic.title}`,
|
`=== BLOG BRIEFING: ${topic.title} ===`,
|
||||||
`Urgency: ${topic.urgency}`,
|
``,
|
||||||
`Source: ${topic.source_type} / ${topic.source}`,
|
`Urgency: ${topic.urgency.toUpperCase()}`,
|
||||||
|
`Source category: ${topic.source_type} | Source: ${topic.source}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (topic.date) lines.push(`Signal date: ${topic.date}`);
|
if (topic.date) lines.push(`Signal date: ${new Date(topic.date).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" })}`);
|
||||||
if (topic.description) lines.push(`Signal summary: ${topic.description}`);
|
|
||||||
if (topic.suggested_angle) lines.push(`Recommended angle: ${topic.suggested_angle}`);
|
// Core signal content — article bullets or summary
|
||||||
if (topic.blog_title_created && topic.last_blog_created_at) {
|
if (topic.description) {
|
||||||
lines.push(`Editorial note: A blog with a very similar title already exists from ${topic.last_blog_created_at}. If used anyway, choose a materially different angle.`);
|
lines.push(``, `--- Market Signals ---`);
|
||||||
|
// Already formatted as bullets for news/nog, or plain summary for market intel
|
||||||
|
lines.push(topic.description.includes("•") ? topic.description : `Signal summary: ${topic.description}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataContext = compactDataContext(topic.data_context);
|
// Recommended editorial angle
|
||||||
if (dataContext) lines.push(`Structured supporting data:\n${dataContext}`);
|
if (topic.suggested_angle) {
|
||||||
|
lines.push(``, `--- Recommended Blog Angle ---`);
|
||||||
|
lines.push(topic.suggested_angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structured data from data_context (vendors, tech, buy signal, etc.)
|
||||||
|
const ctx = topic.data_context;
|
||||||
|
if (ctx) {
|
||||||
|
const extraLines: string[] = [];
|
||||||
|
if (ctx.buy_signal && typeof ctx.buy_signal === "string") {
|
||||||
|
const signalMap: Record<string, string> = { bullish: "BUY signal — demand growing, order soon", bearish: "WAIT signal — pricing softening or supply improving", opportunity: "SHORT-TERM OPPORTUNITY — act now", neutral: "Monitor — no immediate action needed" };
|
||||||
|
extraLines.push(`Buy signal: ${signalMap[ctx.buy_signal] ?? ctx.buy_signal}`);
|
||||||
|
}
|
||||||
|
if (ctx.technologies && String(ctx.technologies).length > 2) extraLines.push(`Key technologies: ${ctx.technologies}`);
|
||||||
|
if (ctx.impact_months) extraLines.push(`Expected market impact: within ${ctx.impact_months} months`);
|
||||||
|
if (extraLines.length > 0) {
|
||||||
|
lines.push(``, `--- Market Context ---`);
|
||||||
|
lines.push(...extraLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.blog_title_created && topic.last_blog_created_at) {
|
||||||
|
lines.push(``, `⚠ Editorial note: A similar blog already exists (created ${new Date(topic.last_blog_created_at).toLocaleDateString("de-DE")}). Choose a materially different angle — different structure, timeframe, or use-case focus.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(``, `--- Writing Instructions ---`);
|
||||||
|
lines.push(`Write a practical optical networking article (600–900 words) that a network engineer or procurement manager at an ISP, cloud provider, or enterprise can immediately use. Include:`);
|
||||||
|
lines.push(`1. What is actually happening in the market (fact-based, no generic intro)`);
|
||||||
|
lines.push(`2. Specific technical implications (form factors, speeds, reach, protocol implications)`);
|
||||||
|
lines.push(`3. Procurement/planning consequences — what to order, what to delay, what to watch`);
|
||||||
|
lines.push(`4. One concrete recommendation or action item`);
|
||||||
|
lines.push(`Do NOT write generic summaries or restate the title. Be opinionated and specific.`);
|
||||||
|
|
||||||
lines.push("Editorial instruction: turn this into a practical optical networking article with procurement/engineering consequences, not a generic news summary.");
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1846,7 +1846,7 @@
|
|||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="margin:0;font-size:1.1rem;color:var(--text-bright)">🏭 Warehouse Stock Intelligence</h2>
|
<h2 style="margin:0;font-size:1.1rem;color:var(--text-bright)">🏭 Warehouse Stock Intelligence</h2>
|
||||||
<p style="margin:0.2rem 0 0;font-size:0.75rem;color:var(--text-dim)">Preise: real (scraped from fs.com · QSFPTEK · NADDOD & more) · Abverkauf: <span style="color:#22c55e;font-weight:600">✅ echte Flexoptix-Verkaufszahlen (intern)</span> · <span style="color:#f59e0b">⚠ Scraper-Lagermengen: DEMO DATA</span></p>
|
<p style="margin:0.2rem 0 0;font-size:0.75rem;color:var(--text-dim)">Preise & Lagermengen: real (scraped from fs.com · QSFPTEK · NADDOD & more) · Abverkauf: <span style="color:#22c55e;font-weight:600">✅ echte Flexoptix-Verkaufszahlen (intern)</span> · <span style="color:#06b6d4">ℹ Scraper-Lagermengen: Wettbewerber-Marktdaten</span></p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="stockLoaded=false;loadStock()" class="btn" style="background:var(--indigo);color:#fff;font-size:0.75rem">↻ Refresh</button>
|
<button onclick="stockLoaded=false;loadStock()" class="btn" style="background:var(--indigo);color:#fff;font-size:0.75rem">↻ Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
@ -1865,17 +1865,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-card" style="text-align:center">
|
<div class="stat-card" style="text-align:center">
|
||||||
<div class="stat-icon" style="color:#6366f1">🇩🇪</div>
|
<div class="stat-icon" style="color:#6366f1">🇩🇪</div>
|
||||||
<div class="stat-label">DE-Lager Total <span style="font-size:0.58rem;color:#f59e0b;font-weight:600">[DEMO]</span></div>
|
<div class="stat-label">DE-Lager Total <span style="font-size:0.58rem;color:#06b6d4;font-weight:500">FS.com</span></div>
|
||||||
<div class="stat-val" id="stock-stat-de">—</div>
|
<div class="stat-val" id="stock-stat-de">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" style="text-align:center">
|
<div class="stat-card" style="text-align:center">
|
||||||
<div class="stat-icon" style="color:#06b6d4">🌍</div>
|
<div class="stat-icon" style="color:#06b6d4">🌍</div>
|
||||||
<div class="stat-label">Global-Lager Total <span style="font-size:0.58rem;color:#f59e0b;font-weight:600">[DEMO]</span></div>
|
<div class="stat-label">Global-Lager Total <span style="font-size:0.58rem;color:#06b6d4;font-weight:500">FS.com</span></div>
|
||||||
<div class="stat-val" id="stock-stat-global">—</div>
|
<div class="stat-val" id="stock-stat-global">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" style="text-align:center">
|
<div class="stat-card" style="text-align:center">
|
||||||
<div class="stat-icon" style="color:#f59e0b">⏳</div>
|
<div class="stat-icon" style="color:#f59e0b">⏳</div>
|
||||||
<div class="stat-label">In Nachlieferung <span style="font-size:0.58rem;color:#f59e0b;font-weight:600">[DEMO]</span></div>
|
<div class="stat-label">In Nachlieferung <span style="font-size:0.58rem;color:#06b6d4;font-weight:500">FS.com</span></div>
|
||||||
<div class="stat-val" id="stock-stat-backorder">—</div>
|
<div class="stat-val" id="stock-stat-backorder">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" style="text-align:center">
|
<div class="stat-card" style="text-align:center">
|
||||||
@ -1986,9 +1986,9 @@
|
|||||||
<tr style="background:var(--surface2)">
|
<tr style="background:var(--surface2)">
|
||||||
<th style="padding:6px 8px;text-align:left;color:var(--text-dim);font-weight:500">Part Number</th>
|
<th style="padding:6px 8px;text-align:left;color:var(--text-dim);font-weight:500">Part Number</th>
|
||||||
<th style="padding:6px 8px;text-align:left;color:var(--text-dim);font-weight:500">Form Factor</th>
|
<th style="padding:6px 8px;text-align:left;color:var(--text-dim);font-weight:500">Form Factor</th>
|
||||||
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Verkauft <span style="font-size:0.6rem;color:#f59e0b;opacity:0.8">[DEMO]</span></th>
|
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Verkauft <span style="font-size:0.6rem;color:#06b6d4;opacity:0.8">FS.com</span></th>
|
||||||
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">DE-Lager <span style="font-size:0.6rem;color:#f59e0b;opacity:0.8">[DEMO]</span></th>
|
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">DE-Lager <span style="font-size:0.6rem;color:#06b6d4;opacity:0.8">FS.com</span></th>
|
||||||
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Global-Lager <span style="font-size:0.6rem;color:#f59e0b;opacity:0.8">[DEMO]</span></th>
|
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Global-Lager <span style="font-size:0.6rem;color:#06b6d4;opacity:0.8">FS.com</span></th>
|
||||||
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Preis (net)</th>
|
<th style="padding:6px 8px;text-align:right;color:var(--text-dim);font-weight:500">Preis (net)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -2026,7 +2026,7 @@
|
|||||||
|
|
||||||
<!-- Recently Restocked -->
|
<!-- Recently Restocked -->
|
||||||
<div class="card" style="overflow:hidden">
|
<div class="card" style="overflow:hidden">
|
||||||
<div style="padding:0.75rem 1rem;border-bottom:1px solid var(--border);font-size:0.85rem;font-weight:600;color:var(--text-bright);display:flex;align-items:center;gap:0.5rem">🆕 Recently Restocked (last 24h) <span style="font-size:0.65rem;font-weight:700;background:#f59e0b22;color:#f59e0b;border:1px solid #f59e0b66;border-radius:3px;padding:1px 6px">DEMO DATA</span></div>
|
<div style="padding:0.75rem 1rem;border-bottom:1px solid var(--border);font-size:0.85rem;font-weight:600;color:var(--text-bright);display:flex;align-items:center;gap:0.5rem">🆕 Recently Restocked (last 24h) <span style="font-size:0.65rem;font-weight:700;background:#06b6d422;color:#06b6d4;border:1px solid #06b6d466;border-radius:3px;padding:1px 6px">SCRAPER DATA</span></div>
|
||||||
<div id="stock-recent" style="padding:1rem;color:var(--text-dim);font-size:0.8rem">No recent restock events</div>
|
<div id="stock-recent" style="padding:1rem;color:var(--text-dim);font-size:0.8rem">No recent restock events</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -4197,6 +4197,7 @@ async function openSwitchDetail(id) {
|
|||||||
h += '<div class="panel-section" id="sw-docs-hdr-' + id + '" style="display:none">Datasheets & Manuals <span id="sw-docs-cnt-' + id + '" style="background:#2563eb18;color:#4287f5;font-size:0.7rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-left:0.4rem"></span></div>';
|
h += '<div class="panel-section" id="sw-docs-hdr-' + id + '" style="display:none">Datasheets & Manuals <span id="sw-docs-cnt-' + id + '" style="background:#2563eb18;color:#4287f5;font-size:0.7rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-left:0.4rem"></span></div>';
|
||||||
h += '<div id="sw-docs-body-' + id + '"></div>';
|
h += '<div id="sw-docs-body-' + id + '"></div>';
|
||||||
|
|
||||||
|
|
||||||
var links = [];
|
var links = [];
|
||||||
if (s.product_page_url) links.push('<a href="' + esc(s.product_page_url) + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;font-weight:600;font-size:0.8rem">Product Page</a>');
|
if (s.product_page_url) links.push('<a href="' + esc(s.product_page_url) + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;font-weight:600;font-size:0.8rem">Product Page</a>');
|
||||||
if (s.datasheet_url) links.push('<a href="' + esc(s.datasheet_url) + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;font-weight:600;font-size:0.8rem">Datasheet</a>');
|
if (s.datasheet_url) links.push('<a href="' + esc(s.datasheet_url) + '" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;font-weight:600;font-size:0.8rem">Datasheet</a>');
|
||||||
@ -5740,6 +5741,8 @@ async function loadBlogLLMStatus() {
|
|||||||
if (foCard) foCard.dataset.foModel = currentFoModel;
|
if (foCard) foCard.dataset.foModel = currentFoModel;
|
||||||
if (foName) foName.textContent = '🎯 ' + currentFoModel;
|
if (foName) foName.textContent = '🎯 ' + currentFoModel;
|
||||||
if (foEnv) foEnv.innerHTML = 'BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=' + currentFoModel;
|
if (foEnv) foEnv.innerHTML = 'BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=' + currentFoModel;
|
||||||
|
// Expose for hot-topics.js (blog pipeline progress label)
|
||||||
|
window._activeFoBlogModel = currentFoModel;
|
||||||
var badge = document.getElementById('blog-llm-status-badge');
|
var badge = document.getElementById('blog-llm-status-badge');
|
||||||
var activeModel = document.getElementById('blog-llm-active-model');
|
var activeModel = document.getElementById('blog-llm-active-model');
|
||||||
var activeProvider = document.getElementById('blog-llm-active-provider');
|
var activeProvider = document.getElementById('blog-llm-active-provider');
|
||||||
@ -7548,9 +7551,9 @@ async function loadStock() {
|
|||||||
return '<tr style="border-bottom:1px solid var(--border)">'
|
return '<tr style="border-bottom:1px solid var(--border)">'
|
||||||
+ '<td style="padding:5px 8px">' + pn + '</td>'
|
+ '<td style="padding:5px 8px">' + pn + '</td>'
|
||||||
+ '<td style="padding:5px 8px;color:var(--text-dim)">' + esc(r.form_factor || '—') + '</td>'
|
+ '<td style="padding:5px 8px;color:var(--text-dim)">' + esc(r.form_factor || '—') + '</td>'
|
||||||
+ '<td style="padding:5px 8px;text-align:right;color:#f59e0b;font-weight:600">' + Number(r.units_sold || 0).toLocaleString() + ' <span style="font-size:0.6rem;opacity:0.6;font-weight:400">demo</span></td>'
|
+ '<td style="padding:5px 8px;text-align:right;color:#f59e0b;font-weight:600">' + Number(r.units_sold || 0).toLocaleString() + '</td>'
|
||||||
+ '<td style="padding:5px 8px;text-align:right;color:#6366f1">' + (r.warehouse_de_qty != null ? Number(r.warehouse_de_qty).toLocaleString() + ' <span style="font-size:0.6rem;opacity:0.5;font-weight:400">demo</span>' : '—') + '</td>'
|
+ '<td style="padding:5px 8px;text-align:right;color:#6366f1">' + (r.warehouse_de_qty != null ? Number(r.warehouse_de_qty).toLocaleString() : '—') + '</td>'
|
||||||
+ '<td style="padding:5px 8px;text-align:right;color:#06b6d4">' + (r.warehouse_global_qty != null ? Number(r.warehouse_global_qty).toLocaleString() + ' <span style="font-size:0.6rem;opacity:0.5;font-weight:400">demo</span>' : '—') + '</td>'
|
+ '<td style="padding:5px 8px;text-align:right;color:#06b6d4">' + (r.warehouse_global_qty != null ? Number(r.warehouse_global_qty).toLocaleString() : '—') + '</td>'
|
||||||
+ '<td style="padding:5px 8px;text-align:right">' + fmtPrice(r.price_net, r.price_currency) + '</td>'
|
+ '<td style="padding:5px 8px;text-align:right">' + fmtPrice(r.price_net, r.price_currency) + '</td>'
|
||||||
+ '</tr>';
|
+ '</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@ -7791,11 +7794,11 @@ async function lookupStock() {
|
|||||||
var latest = obs[0];
|
var latest = obs[0];
|
||||||
resultEl.innerHTML = '<b>' + esc(tx.part_number) + '</b> — '
|
resultEl.innerHTML = '<b>' + esc(tx.part_number) + '</b> — '
|
||||||
+ esc(tx.form_factor || '') + ' ' + esc(tx.speed || '') + '<br>'
|
+ esc(tx.form_factor || '') + ' ' + esc(tx.speed || '') + '<br>'
|
||||||
+ '<span style="color:#6366f1">DE-Lager: ' + (latest.warehouse_de_qty != null ? latest.warehouse_de_qty : '—') + '</span><span style="font-size:0.65rem;color:#f59e0b;margin-left:2px">[demo]</span> · '
|
+ '<span style="color:#6366f1">DE-Lager: ' + (latest.warehouse_de_qty != null ? latest.warehouse_de_qty : '—') + '</span> · '
|
||||||
+ '<span style="color:#06b6d4">Global: ' + (latest.warehouse_global_qty != null ? latest.warehouse_global_qty : '—') + '</span><span style="font-size:0.65rem;color:#f59e0b;margin-left:2px">[demo]</span> · '
|
+ '<span style="color:#06b6d4">Global: ' + (latest.warehouse_global_qty != null ? latest.warehouse_global_qty : '—') + '</span> · '
|
||||||
+ '<span style="color:#f59e0b">Nachlieferung: ' + (latest.backorder_qty != null ? latest.backorder_qty : '—') + '</span><span style="font-size:0.65rem;color:#f59e0b;margin-left:2px">[demo]</span> · '
|
+ '<span style="color:#f59e0b">Nachlieferung: ' + (latest.backorder_qty != null ? latest.backorder_qty : '—') + '</span> · '
|
||||||
+ (latest.price_net != null ? '€' + Number(latest.price_net).toFixed(2) + ' (net)' : '')
|
+ (latest.price_net != null ? '€' + Number(latest.price_net).toFixed(2) + ' (net)' : '')
|
||||||
+ (latest.units_sold != null ? ' · <b>' + latest.units_sold + '×</b> verkauft <span style="font-size:0.65rem;color:#f59e0b">[demo]</span>' : '')
|
+ (latest.units_sold != null ? ' · <b>' + latest.units_sold + '×</b> verkauft' : '')
|
||||||
+ '<br><span style="color:var(--text-dim);font-size:0.7rem">via ' + esc(latest.vendor_name) + ' · ' + new Date(latest.time).toLocaleString('de-DE') + '</span>'
|
+ '<br><span style="color:var(--text-dim);font-size:0.7rem">via ' + esc(latest.vendor_name) + ' · ' + new Date(latest.time).toLocaleString('de-DE') + '</span>'
|
||||||
+ (obs.length > 1 ? ' <span style="color:var(--text-dim)">(' + obs.length + ' observations this week)</span>' : '');
|
+ (obs.length > 1 ? ' <span style="color:var(--text-dim)">(' + obs.length + ' observations this week)</span>' : '');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user