<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Open Source on Özkan Pakdil Software Engineer</title>
    <link>https://ozkanpakdil.github.io/categories/open-source/</link>
    <description>Recent content in Open Source on Özkan Pakdil Software Engineer</description>
    <image>
      <title>Özkan Pakdil Software Engineer</title>
      <url>https://ozkanpakdil.github.io/images/papermod-cover.png</url>
      <link>https://ozkanpakdil.github.io/images/papermod-cover.png</link>
    </image>
    <generator>Hugo -- 0.148.2</generator>
    <language>en</language>
    <copyright>Özkan Pakdil</copyright>
    <lastBuildDate>Tue, 27 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://ozkanpakdil.github.io/categories/open-source/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>First prize at IP2Location 2026: the six-day build log behind geotop</title>
      <link>https://ozkanpakdil.github.io/posts/my_collections/2026/2026-10-27-geotop-ip2location-first-prize/</link>
      <pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://ozkanpakdil.github.io/posts/my_collections/2026/2026-10-27-geotop-ip2location-first-prize/</guid>
      <description>What 34 commits, 6,988 lines of Rust and seven releases in seven hours look like: the architecture, the map renderer, the packaging failures and the release plumbing behind the IP2Location Programming Contest 2026 first prize.</description>
      <content:encoded><![CDATA[<p>The 2026 IP2Location Programming Contest winners are out, and <strong>geotop took first prize</strong>. The award is USD $500 plus a two-year IP2Location.io Security Plan; the <a href="https://contest.ip2location.com/winners">winners page</a> values the whole package at USD $12,476. Second and third went to <a href="https://github.com/mattborja/geotail/">geotail</a> and <a href="https://github.com/mardhiahnetwork/ip2location-sentinel">LocaSentinel</a>.</p>
<p>The short version is easy: geotop won. This is the longer one. I went back through both repositories — 34 commits in <code>geotop</code>, one in <code>GeoSentinel-Ingress</code> — and reconstructed what actually happened between 3 and 9 August 2026: what was deliberate, what was discovered by breaking a release, and what I would change before the next tag.</p>
<p>The contest itself ran from 1 July to 30 September, so these six days sit early in the window rather than at the deadline.</p>
<h2 id="what-geotop-is">What geotop is</h2>
<p><code>geotop</code> is an <code>htop</code>-style monitor that watches raw network packets, nginx/Apache access logs, or both at once, and plots every connection on a live world map. It runs either in the terminal (<code>ratatui</code> + <code>ratatui-image</code>) or as a native window (<code>--gui</code>, eframe/egui).</p>
<table>
  <thead>
      <tr>
          <th>Piece</th>
          <th>Implementation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion</td>
          <td><code>pnet</code> raw datalink capture <strong>and</strong> a Combined/Common Log Format tailer, feeding one normalized event stream</td>
      </tr>
      <tr>
          <td>Enrichment</td>
          <td>IP2Location LITE DB11 + IP2Proxy LITE PX11, mmap-backed lookups via <code>ip2location 0.6</code>, LRU-cached, hot-swappable</td>
      </tr>
      <tr>
          <td>Terminal UI</td>
          <td><code>ratatui 0.30</code> + <code>ratatui-image 11</code>; auto-detects Kitty Graphics → Sixel → half-block</td>
      </tr>
      <tr>
          <td>Native UI</td>
          <td><code>eframe 0.35</code> / <code>egui</code> on <code>wgpu</code>, with zoom, pan, hover details and scalable labels</td>
      </tr>
      <tr>
          <td>Map</td>
          <td>Bundled Natural Earth 110m land polygons, equirectangular projection, pulsing markers, animated arcs</td>
      </tr>
      <tr>
          <td>Operations</td>
          <td>Automatic DB download/extract, token persistence, live config reload, Homebrew cask, crates.io releases</td>
      </tr>
  </tbody>
</table>
<p><img alt="geotop TUI with the world map, top-talkers panel and live log" loading="lazy" src="/posts/my_collections/2026/2026-10-27-geotop-ip2location-first-prize/geotop-tui.webp"></p>
<p><em>The terminal build, using the half-block fallback so it works over SSH and in any terminal.</em></p>
<p>One design decision shaped everything else: <strong>both ingestion sources produce the same event type</strong>. A sniffed packet and a parsed log line disagree about almost every field, but the UI should not care where a connection came from.</p>
<h2 id="six-days-34-commits">Six days, 34 commits</h2>
<table>
  <thead>
      <tr>
          <th>Date</th>
          <th style="text-align: right">Commits</th>
          <th>What landed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3 Aug</td>
          <td style="text-align: right">1 (other repo)</td>
          <td><code>GeoSentinel-Ingress</code> pushed as a single 5,038-insertion commit</td>
      </tr>
      <tr>
          <td>4 Aug, 22:50</td>
          <td style="text-align: right">1</td>
          <td>First <code>geotop</code> commit: 3,327 lines of Rust across ingestion, geo lookup, DB downloader, TUI and GUI — plus a 354-line README</td>
      </tr>
      <tr>
          <td>5 Aug</td>
          <td style="text-align: right">2</td>
          <td>843-line <code>config.rs</code>, vector map renderer (488 → 1,988 lines), home-location detection, screenshots, example config</td>
      </tr>
      <tr>
          <td>7 Aug</td>
          <td style="text-align: right">27</td>
          <td>Release pipeline, animated arcs, map toggle, frame cap, token persistence, six tagged releases before midnight</td>
      </tr>
      <tr>
          <td>8–9 Aug</td>
          <td style="text-align: right">4</td>
          <td><code>0.1.7</code>, Homebrew <code>brew trust</code> docs, cask fixes, SonarCloud scan</td>
      </tr>
  </tbody>
</table>
<p>The first row is the companion repo; the remaining 34 commits are <code>geotop</code>&rsquo;s. The public GitHub repository was created on 5 August at 18:08 UTC — a day after the first local commit. That is not a recommendation, just what the timestamps say.</p>
<p>Two things stand out in the history. First, the initial commit was not a sketch: it already contained the event bus, both ingest paths, the database downloader, the map renderer and both front-ends. The demo had to be runnable end to end before anything else was worth polishing. Second, the second commit is where the architecture got serious: <code>config.rs</code> arrived, the bundled SVG map was replaced with real GeoJSON geometry, and the renderer roughly quadrupled as viewport math, labels and tests moved in.</p>
<p>The arc of the week is easy to read in hindsight: make it work, make it configurable, make it installable, make it understandable to a stranger.</p>
<h3 id="commits-worth-reading">Commits worth reading</h3>
<p>If you want the shape of the work without the prose:</p>
<table>
  <thead>
      <tr>
          <th>Commit</th>
          <th>What it shows</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/221680a"><code>221680a</code></a></td>
          <td>the end-to-end first build: both ingest paths, DB downloader, map, TUI and GUI</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/d420879"><code>d420879</code></a></td>
          <td>config, GeoJSON vector map, home detection, screenshots</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/79ed666"><code>79ed666</code></a></td>
          <td>the animated Bézier connection arcs</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/b8f9dc5"><code>b8f9dc5</code></a></td>
          <td>the Homebrew cask and the macOS notarization job</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/be34532"><code>be34532</code></a></td>
          <td>the keychain partition-list fix that let <code>codesign</code> run in CI</td>
      </tr>
      <tr>
          <td><a href="https://github.com/ozkanpakdil/geotop/commit/90b3edd"><code>90b3edd</code></a></td>
          <td>SonarCloud wiring, after the release dust settled</td>
      </tr>
  </tbody>
</table>
<h2 id="architecture-two-blocking-producers-one-event-bus">Architecture: two blocking producers, one event bus</h2>
<pre tabindex="0"><code>  ┌──────────────────┐        ┌────────────────────┐
  │  pnet datalink   │        │  log tailer        │
  │  (own thread)    │        │  (own thread)      │
  └────────┬─────────┘        └─────────┬──────────┘
           │                            │
           └────────── mpsc ────────────┘
                          │
                  ConnectionEvent
                          │
              ┌───────────▼────────────┐
              │        AppState        │
              │  markers · log · stats │
              └─────┬────────────┬─────┘
                    │            │
               ratatui        eframe/egui
                    │            │
              ┌─────▼────────────▼─────┐
              │  MapRenderer (shared)  │
              │  base raster + dots    │
              └────────────────────────┘
</code></pre><p>Neither <code>pnet</code> nor <code>notify</code> is async-friendly, so both live on dedicated named threads and bridge into Tokio through an <code>mpsc::UnboundedSender&lt;ConnectionEvent&gt;</code>. The UI never blocks on a socket read or a regex.</p>
<p><code>ConnectionEvent</code> is deliberately small: timestamp, source IP, destination port, protocol, HTTP status/method/path, user agent, byte count, source (<code>Pcap</code> or <code>Log</code>) and a severity. Severity is not decoration. Log lines escalate on status (4xx → warn, 5xx → alert); the sniffer escalates on destination port (22/3389/5900 → warn, 23/445/1433 → alert); and the app escalates any IP that reaches 30 hits to warn so a noisy scanner does not stay green.</p>
<p><code>AppState</code> is the single writer for everything the UI reads. It keeps a FIFO of map markers (5,000 max, 8-second TTL by default), a 2,000-row log ring, per-IP counters, per-country counters and a 120-entry throughput ring. The caps are not incidental: they are what keeps a busy access log from turning into an out-of-memory event.</p>
<p>Geolocation sits behind an <code>ArcSwapOption&lt;DB&gt;</code>: lookups read the current <code>Arc</code> with no mutex, while a background tick checks the <code>.BIN</code> mtime every 60 seconds and swaps in a freshly loaded database. Results are cached in a 20,000-entry LRU keyed by <code>IpAddr</code>, because the renderer and the log panel both ask for the same IP on every event. Downloads are pure Rust — <code>reqwest</code> streaming plus the <code>zip</code> crate, no <code>curl</code>/<code>unzip</code> shell-outs — and the download token is persisted to the config file only after the database has been opened successfully, so a typo in a token cannot overwrite a working one. That last detail came directly from running the tool under <code>sudo</code>, which strips environment variables.</p>
<p>The home marker is detected the same way a user would do it manually: ask an IPv4 &ldquo;what is my IP&rdquo; endpoint, look that address up in the local database, fall back to configured coordinates if either step fails. It is best-effort by design; being offline should not stop the monitor from starting.</p>
<h2 id="the-map-is-a-third-of-the-codebase">The map is a third of the codebase</h2>
<table>
  <thead>
      <tr>
          <th>Module</th>
          <th style="text-align: right">Lines today</th>
          <th>Job</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ui/map_renderer.rs</code></td>
          <td style="text-align: right">2,237</td>
          <td>projection, vector rasterization, markers, arcs, labels</td>
      </tr>
      <tr>
          <td><code>ui/gui.rs</code></td>
          <td style="text-align: right">1,033</td>
          <td>eframe window, zoom/pan, hover, GUI log</td>
      </tr>
      <tr>
          <td><code>config.rs</code></td>
          <td style="text-align: right">905</td>
          <td>typed config, defaults, hot reload</td>
      </tr>
      <tr>
          <td><code>main.rs</code></td>
          <td style="text-align: right">635</td>
          <td>CLI, DB/token setup, worker wiring, TUI loop</td>
      </tr>
      <tr>
          <td><code>db_downloader.rs</code></td>
          <td style="text-align: right">420</td>
          <td>download, extract, validate, hot-swap DBs</td>
      </tr>
      <tr>
          <td><code>ui/panels.rs</code></td>
          <td style="text-align: right">397</td>
          <td>htop-style panels and sparkline</td>
      </tr>
      <tr>
          <td><code>ui/app.rs</code></td>
          <td style="text-align: right">374</td>
          <td>TUI state, caps, counters</td>
      </tr>
      <tr>
          <td>ingestion + lookup</td>
          <td style="text-align: right">~800</td>
          <td>pcap, log tailer, event types, geo lookup</td>
      </tr>
      <tr>
          <td>everything else</td>
          <td style="text-align: right">~200</td>
          <td>home detection, layout, modules</td>
      </tr>
  </tbody>
</table>
<p>The renderer earns its size. The bundled map is 135 KB of Natural Earth 110m land polygons embedded with <code>include_bytes!</code>, so a release binary has no external assets and a crate install cannot silently fall back to a blank map. The geometry is rasterized with <code>tiny-skia</code> into a 2048×1024 equirectangular base; that projection makes latitude/longitude → pixel a two-line function, which matters because every marker, label and arc uses the same math.</p>
<p>Per frame the renderer copies the base, plots every live dot with a glow and an outer halo, fades it by age, pulses the home marker, then hands the buffer to <code>ratatui-image</code>. The GUI does not re-rasterize the world every frame either: viewport changes (zoom, pan) rebuild the base, while dots and arcs draw on top of the cached image.</p>
<p>The connection arcs were the fun part. Each arc is a quadratic Bézier whose control point is lifted north in proportion to distance, so short hops stay almost straight and long hops bow like flight paths. An ease-out cubic grows the arc over 0.7 seconds, with a bright head and a small box-blur glow. A cheap axis-aligned bounding-box test skips arcs that cannot intersect the viewport — but only after checking the box, not the target, so an arc to an off-screen node still draws its visible portion. Ten of the project&rsquo;s fifteen unit tests live in this file: projection sanity for known coordinates, country labels landing inside their polygons, marker styles writing pixels, and arcs still visible when the destination is off-screen.</p>
<p>The other five tests cover config parsing and event helpers. There is no UI snapshot testing; at this size, geometry tests bought more confidence per line than pixel diffs would have.</p>
<h2 id="why-rust-held-up">Why Rust held up</h2>
<p>Rust was not the point of the entry, but it is the reason the project fits in one binary and one event pipeline.</p>
<ul>
<li><strong>Ownership made the concurrency reviewable.</strong> The packet sniffer, log tailer, database reloader and two front-ends are all long-lived and share state. Rust forces every one of those boundaries to be explicit: <code>ArcSwapOption&lt;DB&gt;</code> for lock-free database reads, <code>parking_lot::Mutex</code> around the caches, and no way to accidentally keep a borrowed packet buffer after the frame is gone.</li>
<li><strong>No collector in the frame loop.</strong> The renderer rewrites the RGBA map buffer every frame and reuses the allocation with a <code>copy_from_slice</code> blit. There is no garbage collector to introduce a pause in a tool whose whole job is to report time accurately.</li>
<li><strong>The crates replaced subsystems.</strong> <code>ratatui</code> and <code>ratatui-image</code> for the terminal, <code>eframe</code> for the window, <code>pnet</code> for capture, <code>ip2location</code> for the databases, <code>arc-swap</code> for hot reload, <code>reqwest</code> + <code>zip</code> for downloads, <code>cargo-release</code> + <code>git-cliff</code> for the release. Each one is a subsystem I would otherwise have written badly.</li>
<li><strong>Distribution is a build target.</strong> The same source builds for four release targets; the ingress goes further and links a static musl binary onto <code>scratch</code>. That is not a language feature by itself, but it is what the toolchain makes routine.</li>
</ul>
<h2 id="seven-releases-in-seven-hours">Seven releases in seven hours</h2>
<p>Versions 0.1.1 through 0.1.7 were tagged between 16:49 on 7 August and 00:05 on 8 August. That was not release discipline; it was me installing each artifact the way a stranger would and finding the next problem.</p>
<table>
  <thead>
      <tr>
          <th>What broke</th>
          <th>Root cause</th>
          <th>What shipped</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tag came out as <code>vv0.1.1</code></td>
          <td><code>cargo-release</code> already prefixes <code>v</code></td>
          <td>Dropped the custom tag prefix</td>
      </tr>
      <tr>
          <td>Linux release job failed</td>
          <td>eframe/winit features and missing X11/Wayland dev libs</td>
          <td>Enabled <code>x11</code>/<code>wayland</code>; installed only the FFI-linked libs</td>
      </tr>
      <tr>
          <td>Intel macOS build had no runner</td>
          <td>GitHub retired <code>macos-13</code></td>
          <td>Cross-compiled <code>x86_64-apple-darwin</code> on the arm64 runner</td>
      </tr>
      <tr>
          <td><code>cargo install geotop</code> failed on newer rustc</td>
          <td><code>zune-core 0.5.2</code> broke <code>zune-jpeg</code>; <code>cargo install</code> re-resolves deps</td>
          <td>Pinned <code>zune-core = &quot;=0.5.1&quot;</code></td>
      </tr>
      <tr>
          <td>Debug build aborted on <code>--all-interfaces</code> (macOS)</td>
          <td><code>pnet 0.34</code> read <code>bpf_hdr</code> at a misaligned offset</td>
          <td>Upgraded to <code>pnet 0.35</code></td>
      </tr>
      <tr>
          <td>Gatekeeper flagged the downloaded binary</td>
          <td>Unsigned Mach-O</td>
          <td>Universal2 build, Developer ID codesign, notarization</td>
      </tr>
      <tr>
          <td>Token had to be re-supplied under <code>sudo</code></td>
          <td><code>sudo</code> strips environment variables</td>
          <td>Token persisted after a successful download</td>
      </tr>
      <tr>
          <td>GUI uploaded a new map texture every vsync frame</td>
          <td>Animation was uncapped</td>
          <td>Added <code>gui_max_fps</code></td>
      </tr>
  </tbody>
</table>
<p>The last one is the least dramatic and the most typical: the map looked great, and the GPU was uploading a new 2048×1024 texture every vsync frame. A frame cap is not a feature anyone asks for, but it is the difference between a demo and something you leave running.</p>
<p>The release workflow ended up with four jobs:</p>
<ul>
<li><strong>build</strong> — a matrix of Linux x86_64, macOS arm64, macOS x86_64 and Windows MSVC. Windows needs the Npcap SDK headers; Linux needs <code>libpcap-dev</code>, <code>libwayland-dev</code> and <code>pkg-config</code>, and nothing else, because winit&rsquo;s X11 backend uses pure-Rust <code>x11rb</code> and the rest <code>dlopen</code> at runtime.</li>
<li><strong>notarize-macos</strong> — <code>lipo</code> the two Mach-Os into one universal binary, import the Developer ID certificate into a temporary keychain, codesign with the hardened runtime, submit with <code>notarytool --wait</code>, then package the tarball. Without the Apple secrets the job no-ops rather than blocking the release, so unsigned per-arch tarballs still ship.</li>
<li><strong>release</strong> — <code>git-cliff --latest --strip header</code> builds the notes from Conventional Commits, the cask&rsquo;s <code>version</code> and <code>sha256</code> are bumped in place and committed with the default <code>GITHUB_TOKEN</code>, then the GitHub Release is created.</li>
<li><strong>publish-crate</strong> — <code>cargo package</code>, verify the <code>.crate</code> tarball actually contains the GeoJSON, then publish. An &ldquo;already exists&rdquo; response is treated as success so rerunning a release is safe.</li>
</ul>
<p>There is also a paranoid check in the build job: <code>grep</code> the compiled binary for <code>&quot;type&quot;:&quot;FeatureCollection&quot;</code> and fail if it is missing. It sounds excessive until you remember that an asset present in the source tree is not necessarily present in the artifact.</p>
<p>Installing the result should be boring:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew tap ozkanpakdil/geotop https://github.com/ozkanpakdil/geotop
</span></span><span style="display:flex;"><span>brew trust ozkanpakdil/geotop
</span></span><span style="display:flex;"><span>brew install --cask geotop
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or, with a Rust toolchain</span>
</span></span><span style="display:flex;"><span>cargo install geotop
</span></span></code></pre></div><p>The <code>brew trust</code> line is not optional on current Homebrew, and the explicit tap URL is needed because the shorthand <code>ozkanpakdil/geotop</code> resolves to a <code>homebrew-geotop</code> repository by convention. Both facts are in the README because both are easy to get wrong.</p>
<h2 id="geosentinel-ingress-the-companion-that-shipped-first">GeoSentinel-Ingress: the companion that shipped first</h2>
<p><code>GeoSentinel-Ingress</code> is the other entry: an async ingress/reverse-proxy middleware that resolves the real client IP, enriches requests with IP2Location data, evaluates declarative security rules, and exports Prometheus metrics plus a small admin dashboard.</p>
<p><img alt="GeoSentinel-Ingress admin dashboard" loading="lazy" src="/posts/my_collections/2026/2026-10-27-geotop-ip2location-first-prize/geosentinel-dashboard.webp"></p>
<p><em>The admin dashboard: request counters, memory, and a Leaflet heatmap fed by <code>/api/stats</code>.</em></p>
<p>It is built on axum 0.7 / hyper 1 / tower, with a GeoShield middleware that runs before the proxy handler. For every request it resolves the client IP (<code>X-Forwarded-For</code>, then <code>X-Real-IP</code>, then the socket peer), does a geo lookup and a proxy lookup, injects four headers upstream, records histograms and counters, and then evaluates the rule chain:</p>
<ol>
<li><code>allow_countries</code> (if non-empty, anything else is denied),</li>
<li><code>deny_countries</code>,</li>
<li><code>proxy_action</code> / <code>deny_proxy_types</code>,</li>
<li>governor-based rate limits, per IP and optionally per country,</li>
<li><code>default_action</code>.</li>
</ol>
<p>Denied requests get 403 with the reason; <code>challenge</code> gets 401. The upstream sees <code>X-Geo-Country</code>, <code>X-Geo-City</code>, <code>X-Geo-Coordinates</code> and <code>X-Geo-Proxy-Type</code>, with the city sanitized to visible ASCII so a header can never carry control characters.</p>
<p>The rest of the service is deliberately small: a multi-stage Dockerfile that compiles a static musl binary and ships it on <code>scratch</code> (copying <code>rules.yaml</code> and <code>assets/admin.html</code>, because the admin page is read from disk at startup), a Prometheus recorder refreshed every five seconds, a <code>DashMap</code> of per-country allow/deny/challenge counters for <code>/api/stats</code>, and two Criterion benchmarks for lookups and rule evaluation.</p>
<p>The shared DNA with <code>geotop</code> is explicit in the code. <code>geotop</code>&rsquo;s database manager says it is &ldquo;modelled directly on the <code>DatabaseManager</code> from GeoSentinel-Ingress&rdquo;; its Cargo manifest notes that <code>ip2location</code> is &ldquo;kept in sync with GeoSentinel-Ingress 0.1&rdquo;; and both use the same LITE DB filenames, the same <code>token=</code> download convention, pure-Rust zip extraction and <code>arc-swap</code> for hot reload. Building the ingress first meant the monitor inherited a database lifecycle that had already been made to work in a service.</p>
<p>The honest part: GeoSentinel has exactly one commit, <code>59ff933</code>, with 5,038 insertions across 22 files and the message &ldquo;feat: implement configuration management and database handling&rdquo;. That message describes maybe a fifth of what landed. The code is readable; the history is not. <code>geotop</code>&rsquo;s 34 mostly small commits are the reason this post can exist at all.</p>
<h2 id="what-i-would-change-before-the-next-tag">What I would change before the next tag</h2>
<p>A build log without the loose ends would be marketing. These are the ones I can see in the code today:</p>
<ul>
<li><strong>Unify the two UI states.</strong> The TUI and GUI keep near-duplicate <code>AppState</code>/<code>GuiState</code> structs. That duplication is why the same cosmetic bug exists in both: the proxy-share gauge counts per-country keys starting with <code>~</code>, but those keys are never written that way, so it reads 0%. A shared core with two renderers would have prevented it.</li>
<li><strong>Fix the throughput units.</strong> <code>tick()</code> appends a bucket every 100 ms, so the 120-entry ring covers about twelve seconds, while the panel calls it &ldquo;Packets / sec (60s)&rdquo; and <code>events_per_sec</code> divides by the ring length. The shape is right; the units are not.</li>
<li><strong>Do not trust <code>X-Forwarded-For</code> from every peer.</strong> GeoShield takes the first parseable entry from any client. Behind a known proxy that is correct; exposed directly, it is spoofable. A trusted-proxy list is the missing guard.</li>
<li><strong>Stream request bodies.</strong> The proxy buffers the whole body with <code>to_bytes(..., usize::MAX)</code> before forwarding. Fine for JSON APIs, wrong for large uploads.</li>
<li><strong>Run the tests in CI.</strong> <code>build.yml</code> is a SonarCloud scan. The fifteen unit tests only run when I run them locally. A <code>fmt</code> + <code>clippy</code> + <code>test</code> job is the obvious next commit.</li>
<li><strong>Add backpressure.</strong> The event channel is unbounded; a traffic flood can queue faster than the UI drains. A bounded channel with sampling or aggregation would make the memory profile honest under attack.</li>
<li><strong>Finish the packaging.</strong> There is no <code>LICENSE</code> file even though both manifests declare <code>MIT OR Apache-2.0</code>, and GeoSentinel&rsquo;s <code>Cargo.toml</code> still points at <code>yourorg/geosentinel-ingress</code>.</li>
</ul>
<p>None of these change the result of the contest. All of them are the difference between &ldquo;works on my machine&rdquo; and &ldquo;I would run this on a server&rdquo;.</p>
<h2 id="what-i-think-mattered">What I think mattered</h2>
<p>I cannot tell you what the judges weighted, and I would not guess. I can tell you where the time went, and the distribution is the lesson:</p>
<ul>
<li><strong>The first commit was end-to-end.</strong> Not because planning is bad, but because a judge — or any user — needs to run the thing before they can care about the internals.</li>
<li><strong>Much of the work was after the feature set.</strong> Signing, notarization, the cask, the token prompts, the <code>sudo</code> notes, the Gatekeeper section. A repository is not a tool until a stranger can install it without reading your source.</li>
<li><strong>Small commits paid twice.</strong> Conventional Commits made <code>git-cliff</code> release notes possible, and the 34-commit history is the only reason this post can describe the week instead of just the final tree.</li>
<li><strong>Tests around the risky math.</strong> Projection and arc geometry are easy to get subtly wrong and hard to eyeball. The map tests were worth more than broader coverage would have been.</li>
</ul>
<p><code>geotop</code> is still the tool I wanted: point it at an interface or an access log, and watch the world light up. Winning first prize was a genuinely lovely surprise. Thank you to IP2Location for running the contest and for the LITE databases that make local, private geolocation possible.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/ozkanpakdil/geotop"><code>geotop</code></a> — source, releases, Homebrew cask</li>
<li><a href="https://crates.io/crates/geotop"><code>geotop</code> on crates.io</a></li>
<li><a href="https://github.com/ozkanpakdil/GeoSentinel-Ingress"><code>GeoSentinel-Ingress</code></a></li>
<li><a href="https://contest.ip2location.com/winners">IP2Location Programming Contest 2026 winners</a></li>
<li><a href="https://contest.ip2location.com/">Contest home</a></li>
<li><a href="https://lnkd.in/p/ezAjQWnK">Ip2Location Linkedin Post</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
