From 3a3307e19061dd3f3f4b22418559e7d275ac85c5 Mon Sep 17 00:00:00 2001
From: mr0xb <47467008+mr0xb@users.noreply.github.com>
Date: Wed, 26 Aug 2026 22:41:52 -0400
Subject: [PATCH 1/2] fix - carto now requires api keys
---
README.md | 21 ++++++++-
screenshot1.png => assets/screenshot1.png | Bin
screenshot2.png => assets/screenshot2.png | Bin
cmd/root.go | 5 ++
internal/report/report.go | 21 ++++++++-
internal/report/report_test.go | 53 ++++++++++++++++++++++
internal/report/theme_cactus.go | 2 +-
internal/report/theme_cyberpunk.go | 2 +-
internal/report/theme_purplerain.go | 2 +-
9 files changed, 100 insertions(+), 6 deletions(-)
rename screenshot1.png => assets/screenshot1.png (100%)
rename screenshot2.png => assets/screenshot2.png (100%)
diff --git a/README.md b/README.md
index 561b5cd..3d23ff3 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,9 @@
A fast nginx log parser that generates themeable HTML reports with charts, searchable tables, and optional GeoIP2 hit maps.
-
+
-
+
## Features
@@ -54,6 +54,7 @@ nxstats [flags]
| `--no-gzip` | | `false` | Skip `.gz` compressed rotated logs |
| `--split-by-day` | | `false` | Write one HTML report per calendar day plus index |
| `--theme` | | `cyberpunk` | Report visual theme: `cyberpunk`, `purplerain`, `cactus` |
+| `--map-api-key` | | | CARTO basemap API key, appended to tile URLs as `?key=` (env: `NXSTATS_MAP_API_KEY`) |
### Examples
@@ -78,6 +79,12 @@ nxstats --dir /var/log/nginx --split-by-day -o index.html
# Writes index.html + 2024-01-15.html, 2024-01-16.html, etc.
```
+**With a CARTO basemap API key (required for map tiles):**
+```bash
+nxstats --geoip ./GeoLite2-City.mmdb --map-api-key XYZ -o report.html
+# or: NXSTATS_MAP_API_KEY=XYZ nxstats --geoip ./GeoLite2-City.mmdb -o report.html
+```
+
**With a different theme:**
```bash
nxstats --dir /var/log/nginx --theme purplerain -o report.html
@@ -91,6 +98,16 @@ The geographic distribution map requires a free MaxMind GeoLite2-City database.
2. Download `GeoLite2-City.mmdb`
3. Pass the path via `--geoip ./GeoLite2-City.mmdb`
+## Basemap API Key
+
+The map's background tiles come from [CARTO](https://carto.com/), which now
+requires an API key. Create one in the CARTO dashboard and pass it with
+`--map-api-key` (or set `NXSTATS_MAP_API_KEY`); it is appended to the theme's
+tile URL as `?key=...` in the generated report. Note that the key is embedded
+in the HTML output, so treat generated reports as you would the key itself.
+
+Without a key the map still renders, but tiles may come back blank.
+
## Log Format
nxstats expects the standard nginx combined log format for access logs:
diff --git a/screenshot1.png b/assets/screenshot1.png
similarity index 100%
rename from screenshot1.png
rename to assets/screenshot1.png
diff --git a/screenshot2.png b/assets/screenshot2.png
similarity index 100%
rename from screenshot2.png
rename to assets/screenshot2.png
diff --git a/cmd/root.go b/cmd/root.go
index 1867ab6..38b077b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -25,6 +25,7 @@ var (
flagGeoIP string
flagSplitByDay bool
flagTheme string
+ flagMapAPIKey string
)
var rootCmd = &cobra.Command{
@@ -46,6 +47,8 @@ func init() {
rootCmd.Flags().BoolVar(&flagSplitByDay, "split-by-day", false, "Write one HTML report per calendar day plus an index page")
rootCmd.Flags().StringVar(&flagTheme, "theme", report.DefaultTheme,
"Report visual theme ("+strings.Join(report.ThemeNames(), ", ")+")")
+ rootCmd.Flags().StringVar(&flagMapAPIKey, "map-api-key", os.Getenv("NXSTATS_MAP_API_KEY"),
+ "CARTO basemap API key, appended to map tile URLs as ?key= (env: NXSTATS_MAP_API_KEY)")
}
// validateTheme returns an error if name is not a recognized theme.
@@ -108,6 +111,7 @@ func runE(cmd *cobra.Command, args []string) error {
AccessEntries: accessEntries,
ErrorEntries: errorEntries,
Theme: flagTheme,
+ MapAPIKey: flagMapAPIKey,
}
report.ComputeStats(&data)
@@ -212,6 +216,7 @@ func runSplitByDay(
for i := range days {
days[i].Data.IndexFile = indexBase
days[i].Data.DayTitle = days[i].Date.Format("2006-01-02")
+ days[i].Data.MapAPIKey = flagMapAPIKey
// Per-day geo: set Count from this day's access entries
if len(geoPoints) > 0 {
diff --git a/internal/report/report.go b/internal/report/report.go
index 12218be..e5c9216 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"html/template"
+ "net/url"
"path/filepath"
"sort"
"strings"
@@ -66,6 +67,10 @@ type ReportData struct {
// Theme selects the visual theme (see themes.go). Empty or unrecognized
// falls back to DefaultTheme.
Theme string
+
+ // MapAPIKey is the basemap provider API key. When non-empty it is appended
+ // to the theme's tile URL as a "key" query parameter (CARTO's format).
+ MapAPIKey string
}
// DaySummary is one row in the index page's daily breakdown table.
@@ -338,6 +343,20 @@ type reportRenderData struct {
MarkerColor string
}
+// tileURLWithKey appends the basemap provider API key to tileURL as a "key"
+// query parameter, e.g. ".../{z}/{x}/{y}.png?key=XYZ". The URL is returned
+// unchanged when no key is set.
+func tileURLWithKey(tileURL, key string) string {
+ if key == "" || tileURL == "" {
+ return tileURL
+ }
+ sep := "?"
+ if strings.Contains(tileURL, "?") {
+ sep = "&"
+ }
+ return tileURL + sep + "key=" + url.QueryEscape(key)
+}
+
// GenerateHTML renders the full HTML report from data.
func GenerateHTML(data ReportData) (string, error) {
th := themeFor(data.Theme)
@@ -354,7 +373,7 @@ func GenerateHTML(data ReportData) (string, error) {
Chart3: th.Chart3,
ChartWarn: th.ChartWarn,
ChartErr: th.ChartErr,
- MapTileURL: th.MapTileURL,
+ MapTileURL: tileURLWithKey(th.MapTileURL, data.MapAPIKey),
MapAttribution: th.MapAttribution,
MarkerColor: th.MarkerColor,
}
diff --git a/internal/report/report_test.go b/internal/report/report_test.go
index c0f2589..a0e6391 100644
--- a/internal/report/report_test.go
+++ b/internal/report/report_test.go
@@ -624,3 +624,56 @@ func TestGenerateHTML_DayTitleInHeader(t *testing.T) {
t.Error("expected DayTitle in report HTML")
}
}
+
+// ── map API key ────────────────────────────────────────────────────────────
+
+func TestTileURLWithKey(t *testing.T) {
+ cases := []struct{ url, key, want string }{
+ {"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", "XYZ",
+ "https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=XYZ"},
+ {"https://tiles.example/{z}/{x}/{y}.png?style=dark", "XYZ",
+ "https://tiles.example/{z}/{x}/{y}.png?style=dark&key=XYZ"},
+ {"https://tiles.example/{z}/{x}/{y}.png", "a b&c",
+ "https://tiles.example/{z}/{x}/{y}.png?key=a+b%26c"},
+ {"https://tiles.example/{z}/{x}/{y}.png", "",
+ "https://tiles.example/{z}/{x}/{y}.png"},
+ }
+ for _, c := range cases {
+ if got := tileURLWithKey(c.url, c.key); got != c.want {
+ t.Errorf("tileURLWithKey(%q, %q) = %q, want %q", c.url, c.key, got, c.want)
+ }
+ }
+}
+
+func TestGenerateHTML_MapAPIKeyInTileURL(t *testing.T) {
+ data := makeTestData()
+ ComputeStats(&data)
+ data.GeoLocations = []GeoPoint{
+ {IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
+ }
+ data.MapAPIKey = "secret-key"
+
+ html, err := GenerateHTML(data)
+ if err != nil {
+ t.Fatalf("GenerateHTML error: %v", err)
+ }
+ if !strings.Contains(html, "?key=secret-key") {
+ t.Error("expected map API key appended to the tile URL")
+ }
+}
+
+func TestGenerateHTML_NoMapAPIKeyLeavesTileURL(t *testing.T) {
+ data := makeTestData()
+ ComputeStats(&data)
+ data.GeoLocations = []GeoPoint{
+ {IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2},
+ }
+
+ html, err := GenerateHTML(data)
+ if err != nil {
+ t.Fatalf("GenerateHTML error: %v", err)
+ }
+ if strings.Contains(html, "key=") {
+ t.Error("did not expect a key query parameter when MapAPIKey is empty")
+ }
+}
diff --git a/internal/report/theme_cactus.go b/internal/report/theme_cactus.go
index fb0f03c..849a9a6 100644
--- a/internal/report/theme_cactus.go
+++ b/internal/report/theme_cactus.go
@@ -241,7 +241,7 @@ var cactusTheme = themeAssets{
Chart3: "#e0956b",
ChartWarn: "#d99a3d",
ChartErr: "#c96a5b",
- MapTileURL: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
+ MapTileURL: "https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}{r}.png",
MapAttribution: `© CARTO`,
MarkerColor: "#e0956b",
}
diff --git a/internal/report/theme_cyberpunk.go b/internal/report/theme_cyberpunk.go
index 8173ba8..df2e61f 100644
--- a/internal/report/theme_cyberpunk.go
+++ b/internal/report/theme_cyberpunk.go
@@ -258,7 +258,7 @@ var cyberpunkTheme = themeAssets{
Chart3: "#ff00c8",
ChartWarn: "#ffcc00",
ChartErr: "#ff4444",
- MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
+ MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png",
MapAttribution: `© CARTO`,
MarkerColor: "#ff00c8",
}
diff --git a/internal/report/theme_purplerain.go b/internal/report/theme_purplerain.go
index db9020a..36c59a6 100644
--- a/internal/report/theme_purplerain.go
+++ b/internal/report/theme_purplerain.go
@@ -243,7 +243,7 @@ var purplerainTheme = themeAssets{
Chart3: "#e879f9",
ChartWarn: "#fbbf24",
ChartErr: "#fb7185",
- MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
+ MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png",
MapAttribution: `© CARTO`,
MarkerColor: "#a78bfa",
}
From 7115b2711371dafe86038c90564b5de165e48b42 Mon Sep 17 00:00:00 2001
From: mr0xb
Date: Thu, 27 Aug 2026 12:35:06 -0400
Subject: [PATCH 2/2] Update .forgejo/workflows/release.yml
use local actions mirror(s), pin hash
---
.forgejo/workflows/release.yml | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml
index 2146c97..bbc93b7 100644
--- a/.forgejo/workflows/release.yml
+++ b/.forgejo/workflows/release.yml
@@ -16,7 +16,7 @@ jobs:
outputs:
new_tag: ${{ steps.tag.outputs.new_tag }}
steps:
- - uses: https://code.forgejo.org/actions/checkout@v4
+ - uses: https://git.sbriggs.io/actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a
with:
fetch-depth: 0
@@ -71,7 +71,8 @@ jobs:
needs: tag
runs-on: debian
steps:
- - uses: https://code.forgejo.org/actions/checkout@v4
+ #- uses: https://code.forgejo.org/actions/checkout@v4
+ - uses: https://git.sbriggs.io/actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a
with:
fetch-depth: 0
ref: ${{ needs.tag.outputs.new_tag }}
@@ -110,7 +111,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Publish Forgejo release
- uses: https://code.forgejo.org/actions/forgejo-release@v2
+ uses: https://git.sbriggs.io/actions/forgejo-release@585a6614e80a6bcfc429b956ae34831889cabb78
with:
direction: upload
url: ${{ github.server_url }}