function withArrows(steps) {
  return steps.map((s, i) => ({ ...s, hasNext: i < steps.length - 1 }))
}

const ARTICLES = [
  {
    id: 'snmp-to-kafka-aiops',
    visible: true,
    category: 'AIOps',
    title: 'From SNMP Polling to Kafka Streaming and AI: The Evolution of Network Monitoring',
    excerpt: 'Why polling every five minutes leaves a network blind most of the time — and the streaming architecture the whole industry is moving toward.',
    blocks: [
      { p: 'For three decades, SNMP polling has been the default way to watch a network. But polling a router every five minutes means everything that happens between polls — micro-bursts, self-healing BGP flaps, control-plane overload from answering thousands of OIDs — is invisible.' },
      { p: 'The cost of operating this way is no longer just technical, it is financial: network downtime costs organizations an average of $5,600 per minute, with large enterprises losing over $300,000 per hour. That figure alone justifies rethinking the monitoring model.' },
      { h4: 'The market is already voting with its budget' },
      { ul: [
        'The global network telemetry market is projected to grow from roughly $0.97B in 2025 to $4.33B by 2030, a CAGR near 33–40%',
        'AIOps adoption reached 94% of enterprises in 2025, up 12 points year over year, with the market itself projected at $15–19B in 2026 and $37–42B by 2030–2031',
        'Anomaly-detection models based on statistical baselines are cutting Mean Time to Resolution by up to 60%',
        'A mid-size enterprise already generates roughly 1.5 terabytes of telemetry a day — well past what manual analysis can cover',
      ]},
      { p: 'Model-Driven Telemetry over gRPC/gNMI is the natural next step, streaming metrics sub-second instead of waiting on a poll cycle. But the classic starter stack — a router dial-out straight into Telegraf and InfluxDB — breaks down once it moves from a lab into production with dozens of nodes.' },
      { h4: 'Where the classic stack breaks' },
      { ul: [
        'A single collector becomes a monolith and a single point of failure',
        'No buffer means a slow database write drops or backs up incoming metrics',
        'Feeding both a database and an ML model means the collector has to duplicate every stream by hand',
      ]},
      { h4: 'The fix: decouple collection, transport, and consumption' },
      { diagram: withArrows([
        { tag: '01', label: 'Routers / Switches', edgeLabel: 'gNMI dial-out' },
        { tag: '02', label: 'Distributed Collectors', edgeLabel: 'produce events' },
        { tag: '03', label: 'Kafka Event Bus', edgeLabel: 'fan-out' },
        { tag: '04', label: 'ClickHouse / TimescaleDB', edgeLabel: '' },
      ]), caption: 'Streaming telemetry pipeline — collection, transport, and consumption stay independent' },
      { p: 'Kafka is the piece that makes the rest possible. It absorbs traffic spikes without dropping events, and it lets a time-series database and a real-time ML model both read the same stream independently — write once, consume everywhere.' },
      { h4: 'What runs on the stream' },
      { ul: [
        'ClickHouse or TimescaleDB for high-ingest analytical storage and Grafana dashboards',
        'An AIOps engine consuming Kafka directly for anomaly detection and event correlation',
        'Dynamic anomaly detection that replaces static thresholds with pattern-aware baselines',
      ]},
      { h4: 'Where the market goes next' },
      { ul: [
        'Standards like IEEE 802.1Qcw are pushing multi-vendor telemetry interoperability, lowering the cost of switching providers',
        'OT environments (Modbus/DNP3) and low-memory edge devices remain slower to migrate',
        'Cloud-native players like Datadog and Dynatrace are pressuring legacy vendors with unified dashboards and usage-based pricing',
        'Generative-AI workload observability (token consumption, model latency) is emerging as its own telemetry category — the same streaming-plus-AI pattern extending past the network into the applications running on it',
      ]},
      { p: 'The mindset shift matters more than any single tool: a router stops being a device you query and becomes a source of events. Kafka is the bridge that lets AI models consume those events in real time instead of polling a database every second. With 94% of enterprises already running some form of AIOps, staying on pure polling is no longer a conservative choice — it is a measurable disadvantage in MTTR and downtime cost.' },
    ],
  },
  {
    id: 'tls-proxy-legacy-rescue',
    visible: true,
    category: 'Network Security',
    title: 'How a Small TLS Proxy Saved a Mobile Security Launch From a One-Year Delay',
    excerpt: "A legacy provisioning system could not speak modern TLS to a new security API. A lightweight proxy closed the gap in days, not a year — right as the industry's TLS legacy deadline closed in.",
    blocks: [
      { p: "A new mobile security service — phishing and malware filtering through a dedicated APN — was ready to launch. Then integration testing hit a wall: the legacy provisioning system could not complete a TLS handshake with the security platform's API." },
      { h4: 'The root cause' },
      { ul: [
        'The legacy provisioning system ran an old Java stack, capped at outdated TLS 1.0/1.1 cipher suites',
        'The security API required modern TLS 1.2/1.3 ciphers, no exceptions',
      ]},
      { p: 'This was not a vendor being difficult: TLS 1.0/1.1 have been formally deprecated by the IETF (RFC 8996) since 2021, blocked by every major browser since 2020, and banned under PCI DSS, NIST SP 800-52 Rev. 2, and HIPAA. In 2026 the question in the industry is no longer whether to retire these protocols but how fast — Microsoft alone is running final TLS-legacy shutdowns across Meraki, Azure/Office, and Exchange Online this same year.' },
      { p: "The official fix was a full upgrade of the provisioning platform's core — 12 to 18 months of regression testing across dozens of dependent services, with no budget allocated for the year. Taking that path meant freezing the project indefinitely. This is not a rare dilemma: accumulated technical debt grew by an estimated $6 trillion globally between 2012 and 2023 (Oliver Wyman), and the average enterprise loses over $370M a year from its inability to modernize legacy systems efficiently — about $134M of that tied directly to slow, traditional transformation processes." },
      { h4: 'The alternative: a reverse proxy in the middle' },
      { diagram: withArrows([
        { tag: '01', label: 'Legacy Provisioner', edgeLabel: 'TLS 1.0/1.1' },
        { tag: '02', label: 'Reverse Proxy', edgeLabel: 'TLS 1.2/1.3' },
        { tag: '03', label: 'Security API', edgeLabel: '' },
      ]), caption: 'TLS termination and re-encryption isolated in a single lightweight container' },
      { p: 'The provisioner talked to the proxy using the only cipher suites it knew. The proxy terminated that legacy session, then opened a fresh, modern handshake to the security API and relayed the response back. No change touched the provisioning platform itself.' },
      { h4: 'What it delivered' },
      { ul: [
        'The service launched on schedule with zero delay to the commercial roadmap',
        'The infrastructure team got over a year to plan the real upgrade without launch pressure',
        'The provisioning platform stayed untouched — zero regression risk introduced',
      ]},
      { h4: 'Why this pattern became standard in 2026' },
      { ul: [
        'The TLS-legacy grace period is closing across the board — Meraki, Azure Storage, Office 365, and Exchange Online are all running their own TLS 1.0/1.1 shutdowns through 2026',
        "Technical debt is now a balance-sheet problem, not just a systems one: McKinsey puts it at roughly 40% of large enterprises' IT balance sheets",
        'The sidecar/proxy pattern — terminating TLS in NGINX, HAProxy, or Envoy — is now the industry-standard way to absorb exactly this kind of compatibility gap without touching critical systems',
      ]},
      { h4: 'Lessons learned' },
      { ul: [
        'Technical debt always collects eventually — CAST Software put the global backlog at 61 billion person-days of remediation work in 2025. Waiting for "someday" budget stops being a viable strategy',
        'Engineering pragmatism beats perfectionism — but only when paired with documentation and an exit plan. The purist fix was upgrading the provisioner; the pragmatic one was the proxy. What made it safe wasn\'t just choosing pragmatism, it was documenting the workaround clearly and defining the path back toward the "perfect" solution — so the shortcut never became a permanent blind spot',
        'The proxy/sidecar pattern became a standard — decoupled proxies are now a default part of every container architecture we design, not a one-off workaround',
        'AI as an accelerator, not a replacement for judgment — AI tools sped up troubleshooting the handshake failure and generating precise proxy configs, but the engineering call to make was still ours',
      ]},
      { p: "Have you run into a legacy-meets-modern-API collision in your own projects? What pattern did you use to bridge it? Let's talk in the comments." },
      { p: 'Sources: Cisco Meraki Documentation, The Register, Microsoft Learn / Tech Community, SSL Insights (TLS 1.3 Adoption 2026), Pegasystems / Savanta (2025), Oliver Wyman (via Brights.io), CAST Software ("Coding in the Red", 2025), McKinsey (via byteiota.com), HFS Research.' },
    ],
  },
  {
    id: 'rescued-optiplex-rabbit-hole',
    visible: true,
    category: 'Homelab',
    title: "Rescued a Dell Optiplex and Now I Can't Stop",
    excerpt: "A decommissioned Optiplex 3000, Docker, and a rabbit hole I didn't expect.",
    blocks: [
      { p: "It started with a Dell Optiplex 3000 that was about to get wiped and trashed. I grabbed it before it disappeared and figured I'd run a few things on it. That was a mistake — in the best way." },
      { p: "Docker opened a door I didn't know was there. The more I poked around, the more I realized how much is already solved. Need a game server? There's a container for that. Need to monitor your uptime, metrics, logs, and analytics? Container, container, container, container." },
      { p: "That's exactly what I ended up with: <strong>Uptime Kuma</strong> for service uptime, <strong>Netdata</strong> for system metrics, <strong>Umami</strong> for page analytics, and <strong>Dozzle</strong> for container logs. Simple stack, everything in one place, surprisingly complete." },
      { p: "The part that got me was publishing this site. I was genuinely a little nervous — I was exposing something to the internet, inside my own network. Old me would've stopped there. Instead I thought: <em>there's probably a container for the security piece too.</em> Turns out, yeah. Problem solved." },
      { p: "AI has a lot to do with this shift. Things that used to feel out of reach — self-hosting, networking, monitoring — feel a lot more approachable when you can just ask questions and get unstuck fast." },
      { p: "Next goal: more Optiplexes, a Kubernetes cluster, and seeing how far this rabbit hole actually goes. I'll keep you posted." },
    ],
  },
  {
    id: 'survival-sandbox-journey',
    visible: true,
    category: 'Entertainment',
    title: "Survival Sandboxes and the Itch I Can't Scratch",
    excerpt: "From Star Wars Galaxies to Enshrouded — apparently I've always been about building stuff and calling it home.",
    blocks: [
      { p: "I never connected the dots until recently, but there's a clear thread running through every game I've actually sunk time into: you build something, and it feels like yours." },
      { p: "It started with <strong>Star Wars Galaxies</strong> — one of the first MMOs I played. Yeah there was grinding, leveling, the full RPG loop. But what I kept coming back to was base building. There was something about someone walking into your place and saying <em>\"this is cool\"</em> that hit different. That little moment of validation from a stranger in a game. I get it now more than I did then." },
      { p: "Fast forward and I'm playing <strong>Minecraft</strong> with my kid. Graphics are what they are, but the crafting system is genuinely deep. And watching a kid figure out resource chains and basic survival logic — that's actually pretty solid for a young brain. It holds up." },
      { p: "A coworker told me he basically lived on Steam sales, buying whatever looked interesting at a discount. I did the same and landed on <strong>No Man's Sky</strong>. That game broke my brain a little. Procedurally generated solar systems, planets, flora, fauna — 18 quintillion planets. The number is absurd. But strip that away and it's basically Minecraft in space: gather everything or die trying. I put a lot of hours in, but a game that massive is hard to maintain intensity on. Eventually I dialed back." },
      { p: "Steam does this thing where it watches what you play and suggests similar titles. That's how I found <strong>Enshrouded</strong>. Fantasy RPG setting, traditional classes, fluid real-time combat — no turn-based stuff, not my thing — with that same loop of crafting, foraging, and figuring things out. The quest line actually guides you through different biomes in a way that feels structured without being hand-holdy. Only complaint: I wish it was on Nintendo Switch so I could play it in traffic. Half joking." },
      { p: "A coworker mentioned he ran a Minecraft server and something clicked — <em>I wonder if there's a container for Enshrouded.</em> There was. Threw it on the Dell Optiplex that was headed to decommission and it runs without breaking a sweat. A simple Docker Compose file and done. Sometimes working in networking and infrastructure has its perks: you see how the sausage is made and suddenly self-hosting a game server feels less like a project and more like a Tuesday." },
      { p: "How long will the Enshrouded phase last? No idea. But here's the takeaway: if something looks interesting, Google it. There's probably a server image, a mod, a community, or at minimum a Reddit thread that tells you if it's worth your time." },
    ],
  },
]

function renderBlock(block) {
  if (block.p) return `<p class="article-p">${block.p}</p>`
  if (block.h4) return `<h4 class="article-h4">${block.h4}</h4>`
  if (block.ul) {
    return `<ul class="article-ul">${block.ul.map(li => `<li>${li}</li>`).join('')}</ul>`
  }
  if (block.diagram) {
    const steps = block.diagram.map(step => `
      <div class="diagram-step">
        <div class="diagram-box">
          <p class="diagram-tag">${step.tag}</p>
          <p class="diagram-label">${step.label}</p>
        </div>
        ${step.hasNext ? `
          <div class="diagram-arrow">
            <span class="diagram-arrow-glyph">↓</span>
            <span class="diagram-edge-label">${step.edgeLabel}</span>
          </div>
        ` : ''}
      </div>
    `).join('')
    return `
      <figure class="article-figure">
        ${block.caption ? `<figcaption class="article-caption">${block.caption}</figcaption>` : ''}
        <div class="diagram">${steps}</div>
      </figure>
    `
  }
  return ''
}

function renderCard(a) {
  return `
    <article class="card article-card reveal">
      <span class="tag tag-neutral">${a.category}</span>
      <h3 class="card-title">${a.title}</h3>
      <p class="card-desc">${a.excerpt}</p>
      <a class="btn btn-ghost" style="align-self:flex-start;margin-top:auto;padding-left:0" href="#article-${a.id}">Read →</a>
    </article>
  `
}

function renderDetail(a) {
  return `
    <div id="article-${a.id}" class="article-detail" data-slug="${a.id}">
      <div class="article-detail-inner">
        <a class="btn btn-secondary" href="#articles">← Back to articles</a>
        <span class="tag tag-neutral" style="margin-top:32px">${a.category}</span>
        <h2 class="article-title">${a.title}</h2>
        <p class="article-hits" data-hits>&nbsp;</p>
        <div class="article-body">
          ${a.blocks.map(renderBlock).join('')}
        </div>
      </div>
    </div>
  `
}

const HITS_ENDPOINT = 'https://hits.carloselugo.com'

async function trackHit(slug) {
  const el = document.querySelector(`#article-${slug} [data-hits]`)
  if (!el) return
  try {
    const res = await fetch(`${HITS_ENDPOINT}/hit/${slug}`, { method: 'POST' })
    const data = await res.json()
    el.textContent = `${data.count.toLocaleString()} views`
  } catch {
    el.textContent = '' // fail silently, never block the article from rendering
  }
}

function checkHashForArticle() {
  const match = location.hash.match(/^#article-(.+)$/)
  if (match) trackHit(match[1])
}

/** Call this once after inserting renderArticles() into the DOM. */
export function initArticles() {
  checkHashForArticle()
  window.addEventListener('hashchange', checkHashForArticle)
}

export function renderArticles() {
  const visible = ARTICLES.filter(a => a.visible)
  if (visible.length === 0) return ''

  const cards = visible.map(renderCard).join('')
  const details = visible.map(renderDetail).join('')

  return `
    <hr class="hr" />
    <section class="articles" id="articles">
      <div class="container">
        <span class="section-kicker">Articles</span>
        <div class="card-grid articles-list">
          ${cards}
        </div>
      </div>
    </section>
    ${details}

    <style>
      .articles { padding: 64px 0; }
      .article-card { text-decoration: none; }

      /* Detail panels — hidden until targeted via #article-<id> anchor */
      .article-detail {
        display: none;
        padding: 64px 0;
        border-top: 2px solid var(--border-strong);
      }
      .article-detail:target { display: block; }
      /* Hide the list section while a detail panel is open */
      .articles:has(~ .article-detail:target) { display: none; }

      .article-detail-inner { max-width: 720px; margin: 0 auto; padding: 0 24px; }
      .article-title { font-size: clamp(1.75rem, 4vw, 2.5rem); margin: 16px 0 8px; }
      .article-hits { font-size: 0.75rem; color: var(--muted); margin: 0 0 24px; font-family: var(--font-mono); }
      .article-body { font-size: 1rem; line-height: 1.8; color: var(--text); }
      .article-p { margin: 0 0 20px; }
      .article-h4 {
        font-size: 0.85rem;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--accent-700);
        margin: 28px 0 10px;
      }
      .article-ul { padding-left: 20px; margin: 0 0 20px; color: var(--muted-2); }
      .article-ul li { margin-bottom: 6px; }

      .article-figure { margin: 8px 0 32px; border: 2px solid var(--border-strong); padding: 24px; }
      .article-caption {
        font-size: 0.7rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--accent-700);
        margin: 0 0 20px;
        text-align: center;
      }
      .diagram {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        max-width: 420px;
        margin: 0 auto;
      }
      .diagram-box { border: 2px solid var(--text); padding: 14px 18px; }
      .diagram-tag {
        font-size: 0.6rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--accent);
        margin: 0 0 6px;
      }
      .diagram-label { font-family: var(--font-sans); font-weight: 800; font-size: 0.85rem; }
      .diagram-arrow {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px 0 8px 18px;
      }
      .diagram-arrow-glyph { font-size: 1.2rem; color: var(--accent); line-height: 1; }
      .diagram-edge-label { font-size: 0.7rem; color: var(--muted); }
    </style>
  `
}