From 5c7cbe0ccfac0bc69e774d05b46f8f967346bb1c Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Tue, 31 Mar 2026 09:54:33 +0200 Subject: [PATCH] feat(v0.2.6): hot topics + pipeline lock + blog delete + clean external JS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hot Topics: - Dynamic topics from /api/hot-topics loaded in Blog Engine tab - 7 data sources (prices, competitors, hype cycle, news, conferences, research, evergreen) - Urgency badges: BREAKING (red), HOT (orange), TRENDING (yellow), EMERGING (green) Pipeline Lock: - Only 1 generation at a time, 'Pipeline Busy' toast on double-click - Progress bar with step names (external hot-topics.js, no inline hacks) Blog Delete: - DELETE /api/blog/:id endpoint - Delete button (✕) on each blog in list - 'Delete All Templates' button to clean up test drafts Fix: dashboard JS extracted to external hot-topics.js to avoid sed quote hell --- packages/api/src/index.ts | 2 +- packages/api/src/routes/blog.ts | 18 +++ packages/api/src/routes/health.ts | 2 +- packages/dashboard/hot-topics.js | 176 ++++++++++++++++++++++++++++++ packages/dashboard/index.html | 141 +++++------------------- 5 files changed, 222 insertions(+), 117 deletions(-) create mode 100644 packages/dashboard/hot-topics.js diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a9690a8..66f2852 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -69,7 +69,7 @@ app.get("/", (_req, res) => { app.get("/api", (_req, res) => { res.json({ name: "Transceiver Intelligence Platform", - version: "0.2.5", + version: "0.2.6", endpoints: [ "GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=", "GET /api/transceivers/:id", diff --git a/packages/api/src/routes/blog.ts b/packages/api/src/routes/blog.ts index 7aa38c6..2365d5c 100644 --- a/packages/api/src/routes/blog.ts +++ b/packages/api/src/routes/blog.ts @@ -1291,3 +1291,21 @@ blogRouter.get("/feedback/training-data", async (_req: Request, res: Response) = res.json({ entries: result.rows, count: result.rowCount }); } catch (err) { res.status(500).json({ error: "Failed" }); } }); + +// DELETE /api/blog/:id — Delete a blog draft +blogRouter.delete("/:id", async (req: Request, res: Response) => { + try { + // Delete feedback first (FK constraint) + await pool.query("DELETE FROM blog_feedback WHERE blog_id = $1::uuid", [req.params.id]); + const result = await pool.query( + "DELETE FROM blog_drafts WHERE id = $1::uuid RETURNING id, title", + [req.params.id] + ); + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: "Draft not found" }); + } + res.json({ success: true, deleted: result.rows[0] }); + } catch (err) { + res.status(500).json({ success: false, error: (err as Error).message }); + } +}); diff --git a/packages/api/src/routes/health.ts b/packages/api/src/routes/health.ts index 82f6f97..8d2d8d7 100644 --- a/packages/api/src/routes/health.ts +++ b/packages/api/src/routes/health.ts @@ -14,7 +14,7 @@ healthRouter.get("/", async (_req: Request, res: Response) => { res.json({ success: true, status: "healthy", - version: "0.2.5", + version: "0.2.6", uptime: process.uptime(), database: { connected: true, diff --git a/packages/dashboard/hot-topics.js b/packages/dashboard/hot-topics.js new file mode 100644 index 0000000..bc7dc18 --- /dev/null +++ b/packages/dashboard/hot-topics.js @@ -0,0 +1,176 @@ +/** + * Hot Topics + Blog Pipeline UX Enhancement (v0.2.5) + * Loaded after main dashboard script. + * Overrides generateBlog + pollBlogLlm with improved versions. + */ +(function() { + var API = window.API || ''; + var blogPipelineRunning = false; + + var STEP_NAMES = [ + 'Topic Expansion', 'Angle Selection', 'Outline Generation', + 'Master Draft (writing...)', 'Reality Injection', 'Technical Deepening', + 'Opinion Layer', 'Kill AI Tone', 'QA Check', 'Quality Score' + ]; + + // Override generateBlog with pipeline lock + progress UI + window.generateBlog = function(topic, speed, customTitle, customAngle) { + if (blogPipelineRunning) { + if (typeof showToast === 'function') showToast('Pipeline Busy', 'A blog is already being generated. Please wait.'); + return; + } + blogPipelineRunning = true; + + var blogList = document.getElementById('blog-list'); + if (blogList) { + blogList.innerHTML = + '
' + + '
Generating Blog with AI...
' + + '
Starting 10-step Flexoptix Style pipeline...
' + + '
Connecting to LLM (qwen2.5:14b)
' + + '
' + + '
' + + '
0%
' + + '
'; + } + + var body = { topic: topic }; + if (speed) body.speed = speed; + + fetch((API || '') + '/api/blog/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }).then(function(r) { return r.json(); }).then(function(data) { + if (data.success && data.draft) { + var s = document.getElementById('bp-status'); + if (s) s.textContent = 'Template created. LLM pipeline running (~10 min)...'; + var b = document.getElementById('bp-bar'); + if (b) b.style.width = '5%'; + pollPipeline(data.draft.id, 0); + } else { + blogPipelineRunning = false; + if (typeof showToast === 'function') showToast('Error', (data.error || 'Generation failed'), true); + if (typeof loadBlogDrafts === 'function') loadBlogDrafts(); + } + }).catch(function(err) { + blogPipelineRunning = false; + if (typeof showToast === 'function') showToast('Network Error', err.message, true); + if (typeof loadBlogDrafts === 'function') loadBlogDrafts(); + }); + }; + + function pollPipeline(id, attempt) { + if (attempt > 80) { + blogPipelineRunning = false; + if (typeof showToast === 'function') showToast('Timeout', 'Pipeline took too long. Check the blog list.'); + if (typeof loadBlogDrafts === 'function') loadBlogDrafts(); + return; + } + setTimeout(function() { + fetch((API || '') + '/api/blog/' + id).then(function(r) { return r.json(); }).then(function(data) { + var d = data.draft || data; + var gen = d.generated_by || ''; + var steps = d.pipeline_steps_completed || 0; + var done = gen && gen !== 'tip-blog-engine-template' && gen.length > 0; + + if (done) { + // Pipeline complete! + var bar = document.getElementById('bp-bar'); + var pct = document.getElementById('bp-pct'); + var status = document.getElementById('bp-status'); + var step = document.getElementById('bp-step'); + if (bar) bar.style.width = '100%'; + if (pct) pct.textContent = '100%'; + if (status) { status.textContent = 'Blog generated! ' + (d.word_count || '?') + ' words'; status.style.color = '#2d6a4f'; } + if (step) step.textContent = 'Generated by: ' + gen; + blogPipelineRunning = false; + if (typeof showToast === 'function') showToast('Blog Ready!', (d.title || 'Article') + ' — ' + (d.word_count || '?') + ' words'); + setTimeout(function() { + if (typeof loadBlogDrafts === 'function') loadBlogDrafts(); + if (typeof viewBlogDraft === 'function') viewBlogDraft(id); + }, 2000); + } else { + // Still processing + var pctVal = Math.min(95, steps * 10 + 5); + var bar = document.getElementById('bp-bar'); + var pct = document.getElementById('bp-pct'); + var status = document.getElementById('bp-status'); + var step = document.getElementById('bp-step'); + if (bar) bar.style.width = pctVal + '%'; + if (pct) pct.textContent = pctVal + '%'; + if (status) status.textContent = 'Step ' + steps + '/10'; + if (step && steps > 0 && steps <= 10) step.textContent = STEP_NAMES[steps - 1] || 'Processing...'; + pollPipeline(id, attempt + 1); + } + }).catch(function() { pollPipeline(id, attempt + 1); }); + }, 15000); + } + + // Hot topics loader + window.loadHotTopics = function() { + var grid = document.getElementById('hot-topics-grid'); + if (!grid) return; + grid.innerHTML = '
Discovering hot topics...
'; + + fetch((API || '') + '/api/hot-topics').then(function(r) { return r.json(); }).then(function(data) { + if (!data.topics || data.topics.length === 0) { + grid.innerHTML = '
Hype Cycle Analysis
'; + return; + } + var colors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' }; + grid.innerHTML = data.topics.slice(0, 6).map(function(t) { + var c = colors[t.urgency] || '#888'; + var escaped = encodeURIComponent(t.blog_type || 'hype_cycle'); + var title = (t.title || '').replace(/'/g, "\\'").replace(/"/g, '"'); + return '
' + + '
' + + '' + t.urgency + '' + + '' + (t.source_type || '') + '
' + + '
' + t.title + '
' + + '
' + ((t.suggested_angle || t.description || '')).slice(0, 80) + '
' + + '
'; + }).join(''); + }).catch(function() { + grid.innerHTML = + '
Hype Cycle
' + + '
Comparison
' + + '
Tutorial
'; + }); + }; + + // Auto-load hot topics when blog tab activates + var origActivateTab = window.activateTab; + if (origActivateTab) { + window.activateTab = function(tabName) { + origActivateTab(tabName); + if (tabName === 'blog') loadHotTopics(); + }; + } +})(); + +// Delete a single blog draft +window.deleteBlogDraft = function(id, title) { + if (!confirm('Delete "' + title + '"?')) return; + fetch((window.API || '') + '/api/blog/' + id, { method: 'DELETE' }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.success) { + if (typeof showToast === 'function') showToast('Deleted', title); + if (typeof loadBlogDrafts === 'function') loadBlogDrafts(); + } + }); +}; + +// Delete all template drafts (keep LLM-generated ones) +window.deleteAllTemplateDrafts = function() { + if (!confirm('Delete ALL template drafts? LLM-generated articles will be kept.')) return; + fetch((window.API || '') + '/api/blog').then(function(r) { return r.json(); }).then(function(data) { + var templates = (data.drafts || []).filter(function(d) { return d.generated_by === 'tip-blog-engine-template' || !d.generated_by; }); + var count = 0; + templates.forEach(function(d) { + fetch((window.API || '') + '/api/blog/' + d.id, { method: 'DELETE' }).then(function() { count++; if (count === templates.length && typeof loadBlogDrafts === 'function') loadBlogDrafts(); }); + }); + if (typeof showToast === 'function') showToast('Cleaning', 'Deleting ' + templates.length + ' template drafts...'); + }); +}; diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index 0c49900..a903615 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -873,14 +873,14 @@