From d824dd7b6d9e7efd68fc2a65ef1b5fe3793e7fe8 Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 21:31:27 -0400 Subject: [PATCH 1/9] forgejo release workflow --- .forgejo/workflows/release.yml | 135 +++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .forgejo/workflows/release.yml diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..e2ba980 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + +# Requires Forgejo v12+; older versions ignore this block. +concurrency: + group: release + cancel-in-progress: false + +jobs: + tag: + runs-on: ubuntu-latest + outputs: + new_tag: ${{ steps.tag.outputs.new_tag }} + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Bump version and push tag + id: tag + shell: bash + env: + DEFAULT_BUMP: patch + run: | + set -euo pipefail + git config user.name "forgejo-actions[bot]" + git config user.email "forgejo-actions[bot]@noreply.localhost" + + # Already tagged? Reuse it instead of creating a duplicate. + if existing=$(git describe --exact-match --tags HEAD 2>/dev/null); then + echo "HEAD already tagged as $existing" + echo "new_tag=$existing" >> "$GITHUB_OUTPUT" + exit 0 + fi + + latest=$(git tag -l 'v[0-9]*' --sort=-v:refname | head -n1) + if [ -z "$latest" ]; then + major=0; minor=0; patch=0 + log=$(git log --pretty=%B) + else + ver=${latest#v} + IFS=. read -r major minor patch <<< "$ver" + log=$(git log --pretty=%B "$latest..HEAD") + fi + + bump="$DEFAULT_BUMP" + case "$log" in + *'#major'*) bump=major ;; + *'#minor'*) bump=minor ;; + *'#patch'*) bump=patch ;; + esac + + case "$bump" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + + new_tag="v${major}.${minor}.${patch}" + echo "Bumping ${latest:-} -> $new_tag ($bump)" + + git tag -a "$new_tag" -m "$new_tag" + git push origin "$new_tag" + echo "new_tag=$new_tag" >> "$GITHUB_OUTPUT" + + build-and-release: + needs: tag + runs-on: ubuntu-latest + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.tag.outputs.new_tag }} + + - uses: https://code.forgejo.org/actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build release binaries + run: make build + + - name: Install UPX + run: | + set -eux + SUDO="" + [ "$(id -u)" -ne 0 ] && SUDO=sudo + $SUDO apt-get update + $SUDO apt-get install -y upx-ucl + + - name: Compress Linux binaries + run: upx --best --lzma dist/*-linux-* + + - name: Generate release notes + id: notes + shell: bash + env: + TAG: ${{ needs.tag.outputs.new_tag }} + run: | + set -euo pipefail + prev=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -vFx "$TAG" | head -n1) + if [ -n "$prev" ]; then + range="$prev..$TAG" + header="## Changes since $prev" + else + range="$TAG" + header="## Changes" + fi + { + echo "notes<<__EOF__" + echo "$header" + echo + git log --no-merges --pretty='- %s (%h)' "$range" + echo "__EOF__" + } >> "$GITHUB_OUTPUT" + + - name: Publish Forgejo release + uses: https://code.forgejo.org/actions/forgejo-release@v2 + with: + direction: upload + url: ${{ github.server_url }} + repo: ${{ github.repository }} + tag: ${{ needs.tag.outputs.new_tag }} + sha: ${{ github.sha }} + token: ${{ secrets.GITHUB_TOKEN }} + release-dir: dist + release-notes: ${{ steps.notes.outputs.notes }} + override: true + verbose: true \ No newline at end of file From 934067a0a3d3005d600827c05b11f7944aba03d6 Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 21:37:38 -0400 Subject: [PATCH 2/9] Update .github/workflows/release.yml.OLD --- .github/workflows/{release.yml => release.yml.OLD} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{release.yml => release.yml.OLD} (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml.OLD similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release.yml.OLD From c21d626d4e2492fa3c6aeceb93b7fedac7447a30 Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 21:38:52 -0400 Subject: [PATCH 3/9] Update .forgejo/workflows/release.yml --- .forgejo/workflows/release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index e2ba980..57d2b15 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -5,9 +5,6 @@ on: branches: - main -permissions: - contents: write - # Requires Forgejo v12+; older versions ignore this block. concurrency: group: release @@ -15,7 +12,7 @@ concurrency: jobs: tag: - runs-on: ubuntu-latest + runs-on: debian outputs: new_tag: ${{ steps.tag.outputs.new_tag }} steps: From 101b6c306aade2a340f4bbe35dd616d385a10ad5 Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 21:48:33 -0400 Subject: [PATCH 4/9] Update .forgejo/workflows/release.yml --- .forgejo/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 57d2b15..a661497 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -69,7 +69,7 @@ jobs: build-and-release: needs: tag - runs-on: ubuntu-latest + runs-on: debian steps: - uses: https://code.forgejo.org/actions/checkout@v4 with: From abe2685808671dea2359e71a2c9c0c712c56910f Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 21:51:40 -0400 Subject: [PATCH 5/9] Update .forgejo/workflows/release.yml --- .forgejo/workflows/release.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a661497..2146c97 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -83,14 +83,6 @@ jobs: - name: Build release binaries run: make build - - name: Install UPX - run: | - set -eux - SUDO="" - [ "$(id -u)" -ne 0 ] && SUDO=sudo - $SUDO apt-get update - $SUDO apt-get install -y upx-ucl - - name: Compress Linux binaries run: upx --best --lzma dist/*-linux-* From 7bafc6b5d4e6044da6159af488beb56b1c0799aa Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 22:13:29 -0400 Subject: [PATCH 6/9] revert abe2685808671dea2359e71a2c9c0c712c56910f revert Update .forgejo/workflows/release.yml --- .forgejo/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 2146c97..a661497 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -83,6 +83,14 @@ jobs: - name: Build release binaries run: make build + - name: Install UPX + run: | + set -eux + SUDO="" + [ "$(id -u)" -ne 0 ] && SUDO=sudo + $SUDO apt-get update + $SUDO apt-get install -y upx-ucl + - name: Compress Linux binaries run: upx --best --lzma dist/*-linux-* From 9cfec42104136d901a07ad088d3d0fb36dfbcdbd Mon Sep 17 00:00:00 2001 From: mr0xb Date: Wed, 26 Aug 2026 22:18:00 -0400 Subject: [PATCH 7/9] Update .forgejo/workflows/release.yml --- .forgejo/workflows/release.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a661497..2146c97 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -83,14 +83,6 @@ jobs: - name: Build release binaries run: make build - - name: Install UPX - run: | - set -eux - SUDO="" - [ "$(id -u)" -ne 0 ] && SUDO=sudo - $SUDO apt-get update - $SUDO apt-get install -y upx-ucl - - name: Compress Linux binaries run: upx --best --lzma dist/*-linux-* 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 8/9] 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. -![Dashboard overview](screenshot1.png) +![Dashboard overview](assets/screenshot1.png) -![Access log table](screenshot2.png) +![Access log table](assets/screenshot2.png) ## 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 9/9] 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 }}