feat: Changelog — CHANGELOG_PENDING.md, /api/changelog route, Overview tab widget

- CHANGELOG_PENDING.md: 26 entries from v0.1.0 to today in JSON-line format
- GET /api/changelog: parses and serves entries as JSON array
- Overview tab: changelog card with type badges (FEAT/FIX/UI/DATA/AI/INFRA),
  dates, show recent/all toggle
This commit is contained in:
Rene Fichtmueller 2026-04-01 22:14:14 +02:00
parent 681da54523
commit dad4750a86
4 changed files with 134 additions and 0 deletions

36
CHANGELOG_PENDING.md Normal file
View File

@ -0,0 +1,36 @@
# TIP Changelog
Format: `{"d":"YYYY-MM-DD","t":"TYPE","m":"Description"}`
Types: FEAT · FIX · UI · DATA · AI · INFRA
---
{"d":"2026-04-01","t":"FEAT","m":"Procurement Intelligence Engine (WS0c): stock_snapshots, abc_classification, reorder_signals, product_lifecycle_events, market_intelligence tables"}
{"d":"2026-04-01","t":"FEAT","m":"Crawler LLM: Ollama-based two-stage extractor (page type detection + structured product extraction) with vendor profiles for 7 vendors"}
{"d":"2026-04-01","t":"FEAT","m":"ABC classification: dynamic A/B/C turnover scoring from price observations, compatibility breadth, vendor count — computed daily"}
{"d":"2026-04-01","t":"FEAT","m":"Reorder signals: buy_now/wait/hold/monitor with signal strength and reasons — computed daily from stock trends, price trends, lead times"}
{"d":"2026-04-01","t":"DATA","m":"Market intelligence seeded: OFC 2026, AWS CapEx $105B, Azure CapEx $80B+, Coherent 400G ZR+ lead times 16-20w, EU TED €2.1B tenders, ECOC 2026, IEEE 802.3df"}
{"d":"2026-04-01","t":"DATA","m":"Lifecycle events seeded: Cisco SFP-10G-LR EOL 2026-06-30, Juniper SFPP-10GE-ER EOL 2026-09-01, 400ZR ratified, 800G MSA draft"}
{"d":"2026-04-01","t":"UI","m":"Procurement Intel tab: Reorder Signals, ABC Classes table, Market Intel cards, Lifecycle Events — live on dashboard"}
{"d":"2026-04-01","t":"FEAT","m":"Market intelligence scraper: OFC/ECOC, IEEE 802.3, EU TED, Farnell/Mouser lead times, LightReading, FierceTelecom — weekly via pg-boss"}
{"d":"2026-04-01","t":"FIX","m":"Dashboard: garbage product names (scraped-*, All Optical Transceivers) no longer shown as product titles — isGarbageName() filter"}
{"d":"2026-04-01","t":"FIX","m":"Dashboard: competitor comparable prices shown as inline tooltip (ⓘ) instead of block element breaking price row layout"}
{"d":"2026-04-01","t":"UI","m":"Dashboard: 100% VERIFIED badge with white-on-green sub-items (Price ✓, Image ✓, Details ✓) — explicit === true checks, no false positives"}
{"d":"2026-04-01","t":"UI","m":"Dashboard list view: SKU + descriptive name on two lines, Verified column with ★ 100% badge"}
{"d":"2026-04-01","t":"UI","m":"Dashboard detail view: manufacturer product name above image, temperature range decoded (COM → 070°C), close button visible on light background"}
{"d":"2026-04-01","t":"DATA","m":"Migration 018: garbage data cleanup — marks scraped-* and category-page scrapes as data_confidence=garbage"}
{"d":"2026-04-01","t":"FEAT","m":"Migration 017: verification tags — price_verified, image_verified, details_verified, fully_verified columns + compute_transceiver_verification() function"}
{"d":"2026-03-31","t":"DATA","m":"Migration 016: data_confidence scoring (garbage/low/medium/high)"}
{"d":"2026-03-31","t":"FEAT","m":"Migration 013: v0.2.0 Sales Intelligence tables — competitor_alerts, price_changes, generated_datasheets, sales_forecasts, blog_posts_v2"}
{"d":"2026-03-31","t":"FEAT","m":"Transport planner route: GET /api/transport — city-pair fiber route recommendations with switch and transceiver BOM"}
{"d":"2026-03-31","t":"FEAT","m":"Blog Engine v2: market_alert, migration_guide, competitor_analysis, buying_guide types with data enrichment pipeline"}
{"d":"2026-03-31","t":"FEAT","m":"Competitor alerts route: GET /api/competitor-alerts — price changes, new products, stock events with acknowledge workflow"}
{"d":"2026-03-30","t":"FEAT","m":"Switch→Flexoptix Finder: GET /api/finder — enter switch model, get matching Flexoptix transceivers with prices and shop links"}
{"d":"2026-03-30","t":"FEAT","m":"MCP Server: 12 tools including find_transceiver, get_compatibility, get_hype_cycle, generate_blog, plan_transport"}
{"d":"2026-03-30","t":"FEAT","m":"Norton-Bass Hype Cycle engine: multigenerational diffusion model for 15 transceiver technologies with adoption curves"}
{"d":"2026-03-30","t":"DATA","m":"440 switches seeded including Cisco, Arista, Juniper, Edgecore, Mellanox, whitebox OCP switches"}
{"d":"2026-03-30","t":"DATA","m":"33,993 compatibility entries — transceiver↔switch compatibility matrix"}
{"d":"2026-03-30","t":"FEAT","m":"Price monitoring: 23 scrapers, 60+ data sources, pg-boss scheduler with 2h/4h/6h/8h cycles — R-SCAN permanent monitoring"}
{"d":"2026-03-30","t":"FEAT","m":"Qdrant vector DB integration: hybrid full-text + semantic search across products, FAQ, datasheets, news"}
{"d":"2026-03-30","t":"INFRA","m":"Stack deployed: PostgreSQL 17 + TimescaleDB port 5433, Qdrant, Cloudflare R2 for images, PM2 on Erik (IONOS VPS)"}
{"d":"2026-03-30","t":"DATA","m":"v0.1.0: 5,018 transceivers, 351 vendors seeded from 23 initial scrapers"}

View File

@ -21,6 +21,7 @@ import { datasheetRouter } from "./routes/datasheets";
import { hotTopicsRouter } from "./routes/hot-topics";
import { adoptionRouter } from "./routes/adoption";
import { procurementRouter } from "./routes/procurement";
import { changelogRouter } from "./routes/changelog";
const app = express();
@ -58,6 +59,7 @@ app.use("/api/datasheets", datasheetRouter);
app.use("/api/adoption", adoptionRouter);
app.use("/api/hot-topics", hotTopicsRouter);
app.use("/api/procurement", procurementRouter);
app.use("/api/changelog", changelogRouter);
// Dashboard (static HTML)
app.use("/dashboard", express.static(join(__dirname, "..", "..", "dashboard")));

View File

@ -0,0 +1,30 @@
/**
* GET /api/changelog Serves CHANGELOG_PENDING.md as parsed JSON.
* Each line of the form {"d":"...","t":"...","m":"..."} is returned as an array.
*/
import { Router, Request, Response } from "express";
import { readFileSync } from "fs";
import { join } from "path";
export const changelogRouter = Router();
const CHANGELOG_PATH = join(__dirname, "..", "..", "..", "..", "CHANGELOG_PENDING.md");
changelogRouter.get("/", (_req: Request, res: Response) => {
try {
const raw = readFileSync(CHANGELOG_PATH, "utf-8");
const entries = raw
.split("\n")
.filter((line) => line.trim().startsWith("{"))
.map((line) => {
try { return JSON.parse(line.trim()); }
catch { return null; }
})
.filter(Boolean);
res.json({ entries, total: entries.length });
} catch (err) {
console.error("Changelog read error:", err);
res.status(500).json({ error: "Could not read changelog" });
}
});

View File

@ -640,6 +640,27 @@
.compare-best { background: var(--green-light); font-weight: 600; }
.compare-cb { width: 16px; height: 16px; cursor: pointer; accent-color: var(--purple); }
/* === CHANGELOG === */
.cl-entry {
display: flex; gap: 0.6rem; align-items: baseline;
padding: 0.4rem 0; border-bottom: 1px solid var(--border);
font-size: 0.78rem;
}
.cl-entry:last-child { border-bottom: none; }
.cl-date { font-family: var(--mono); font-size: 0.68rem; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; }
.cl-type {
font-size: 0.62rem; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.07em; padding: 1px 6px; border-radius: 3px;
white-space: nowrap; flex-shrink: 0;
}
.cl-FEAT { background: rgba(255,129,0,0.12); color: var(--accent); }
.cl-FIX { background: rgba(193,18,31,0.1); color: #c1121f; }
.cl-UI { background: rgba(124,92,252,0.1); color: #7c5cfc; }
.cl-DATA { background: rgba(45,106,79,0.1); color: #2d6a4f; }
.cl-AI { background: rgba(26,26,46,0.1); color: #1a1a2e; }
.cl-INFRA { background: rgba(136,136,136,0.1);color: #666; }
.cl-msg { color: var(--text); line-height: 1.4; }
/* === PROCUREMENT TAB === */
.proc-btn {
background: var(--surface2); border: 1px solid var(--border);
@ -773,6 +794,17 @@
<div class="card-label">API Endpoints</div>
<div id="endpoints-list" class="endpoint-grid mt"></div>
</div>
<!-- CHANGELOG -->
<div class="card mt" id="changelog-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
<div class="card-label" style="margin-bottom:0">Changelog</div>
<div style="display:flex;gap:0.4rem;align-items:center">
<span id="changelog-total" style="font-size:0.7rem;color:var(--text-dim);font-family:var(--mono)"></span>
<button onclick="toggleChangelog()" id="changelog-toggle-btn" style="background:var(--surface2);border:1px solid var(--border);padding:2px 10px;border-radius:5px;cursor:pointer;font-size:0.72rem;color:var(--text-dim)">Show all</button>
</div>
</div>
<div id="changelog-list"></div>
</div>
</div>
<!-- SEARCH -->
@ -3014,6 +3046,39 @@ el('compare-overlay').addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('visible');
});
// ─── CHANGELOG ───────────────────────────────────────────────────────────────
var changelogEntries = [];
var changelogExpanded = false;
async function loadChangelog() {
try {
var d = await api('/api/changelog');
changelogEntries = d.entries || [];
el('changelog-total').textContent = changelogEntries.length + ' entries';
renderChangelog();
} catch(e) {
el('changelog-list').innerHTML = '<div style="color:var(--text-dim);font-size:0.78rem">Not available in preview — runs on production server.</div>';
}
}
function toggleChangelog() {
changelogExpanded = !changelogExpanded;
el('changelog-toggle-btn').textContent = changelogExpanded ? 'Show recent' : 'Show all';
renderChangelog();
}
function renderChangelog() {
var entries = changelogExpanded ? changelogEntries : changelogEntries.slice(0, 8);
el('changelog-list').innerHTML = entries.map(function(e) {
return '<div class="cl-entry">'
+ '<span class="cl-date">' + esc(e.d) + '</span>'
+ '<span class="cl-type cl-' + esc(e.t) + '">' + esc(e.t) + '</span>'
+ '<span class="cl-msg">' + esc(e.m) + '</span>'
+ '</div>';
}).join('');
}
// ─── PROCUREMENT INTEL ───────────────────────────────────────────────────────
var procCurrentSignalFilter = '';
@ -3220,6 +3285,7 @@ async function loadProcLifecycle() {
// INIT
loadOverview();
loadChangelog();
</script>
<script src="/dashboard/hot-topics.js"></script>
</body>