Compare commits
12 changed files with 12 additions and 237 deletions
|
|
@ -1,125 +0,0 @@
|
||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
# Requires Forgejo v12+; older versions ignore this block.
|
|
||||||
concurrency:
|
|
||||||
group: release
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag:
|
|
||||||
runs-on: debian
|
|
||||||
outputs:
|
|
||||||
new_tag: ${{ steps.tag.outputs.new_tag }}
|
|
||||||
steps:
|
|
||||||
- uses: https://git.sbriggs.io/actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a
|
|
||||||
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:-<none>} -> $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: debian
|
|
||||||
steps:
|
|
||||||
#- 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 }}
|
|
||||||
|
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- name: Build release binaries
|
|
||||||
run: make build
|
|
||||||
|
|
||||||
- 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://git.sbriggs.io/actions/forgejo-release@585a6614e80a6bcfc429b956ae34831889cabb78
|
|
||||||
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
|
|
||||||
|
|
@ -46,12 +46,6 @@ jobs:
|
||||||
- name: Build release binaries
|
- name: Build release binaries
|
||||||
run: make build
|
run: make build
|
||||||
|
|
||||||
- name: Install UPX
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y upx-ucl
|
|
||||||
|
|
||||||
- name: Compress Linux binaries
|
|
||||||
run: upx --best --lzma dist/*-linux-*
|
|
||||||
|
|
||||||
- name: Publish GitHub release
|
- name: Publish GitHub release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
12
Makefile
12
Makefile
|
|
@ -18,11 +18,11 @@ all: test build
|
||||||
## build — compile for all target platforms
|
## build — compile for all target platforms
|
||||||
build: $(TARGETS)
|
build: $(TARGETS)
|
||||||
|
|
||||||
#darwin/amd64:
|
darwin/amd64:
|
||||||
# GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-darwin-amd64 .
|
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-darwin-amd64 .
|
||||||
|
|
||||||
#darwin/arm64:
|
darwin/arm64:
|
||||||
# GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-darwin-arm64 .
|
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-darwin-arm64 .
|
||||||
|
|
||||||
linux/amd64:
|
linux/amd64:
|
||||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-amd64 .
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-amd64 .
|
||||||
|
|
@ -30,8 +30,8 @@ linux/amd64:
|
||||||
linux/arm64:
|
linux/arm64:
|
||||||
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-arm64 .
|
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-arm64 .
|
||||||
|
|
||||||
#linux/arm:
|
linux/arm:
|
||||||
# GOOS=linux GOARCH=arm GOARM=7 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-armv7 .
|
GOOS=linux GOARCH=arm GOARM=7 go build $(LDFLAGS) -o $(OUTDIR)/$(BINARY)-linux-armv7 .
|
||||||
|
|
||||||
## test — run all tests
|
## test — run all tests
|
||||||
test:
|
test:
|
||||||
|
|
|
||||||
21
README.md
21
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.
|
A fast nginx log parser that generates themeable HTML reports with charts, searchable tables, and optional GeoIP2 hit maps.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -54,7 +54,6 @@ nxstats [flags]
|
||||||
| `--no-gzip` | | `false` | Skip `.gz` compressed rotated logs |
|
| `--no-gzip` | | `false` | Skip `.gz` compressed rotated logs |
|
||||||
| `--split-by-day` | | `false` | Write one HTML report per calendar day plus index |
|
| `--split-by-day` | | `false` | Write one HTML report per calendar day plus index |
|
||||||
| `--theme` | | `cyberpunk` | Report visual theme: `cyberpunk`, `purplerain`, `cactus` |
|
| `--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
|
### Examples
|
||||||
|
|
||||||
|
|
@ -79,12 +78,6 @@ nxstats --dir /var/log/nginx --split-by-day -o index.html
|
||||||
# Writes index.html + 2024-01-15.html, 2024-01-16.html, etc.
|
# 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:**
|
**With a different theme:**
|
||||||
```bash
|
```bash
|
||||||
nxstats --dir /var/log/nginx --theme purplerain -o report.html
|
nxstats --dir /var/log/nginx --theme purplerain -o report.html
|
||||||
|
|
@ -98,16 +91,6 @@ The geographic distribution map requires a free MaxMind GeoLite2-City database.
|
||||||
2. Download `GeoLite2-City.mmdb`
|
2. Download `GeoLite2-City.mmdb`
|
||||||
3. Pass the path via `--geoip ./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
|
## Log Format
|
||||||
|
|
||||||
nxstats expects the standard nginx combined log format for access logs:
|
nxstats expects the standard nginx combined log format for access logs:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ var (
|
||||||
flagGeoIP string
|
flagGeoIP string
|
||||||
flagSplitByDay bool
|
flagSplitByDay bool
|
||||||
flagTheme string
|
flagTheme string
|
||||||
flagMapAPIKey string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
|
@ -47,8 +46,6 @@ func init() {
|
||||||
rootCmd.Flags().BoolVar(&flagSplitByDay, "split-by-day", false, "Write one HTML report per calendar day plus an index page")
|
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,
|
rootCmd.Flags().StringVar(&flagTheme, "theme", report.DefaultTheme,
|
||||||
"Report visual theme ("+strings.Join(report.ThemeNames(), ", ")+")")
|
"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.
|
// validateTheme returns an error if name is not a recognized theme.
|
||||||
|
|
@ -111,7 +108,6 @@ func runE(cmd *cobra.Command, args []string) error {
|
||||||
AccessEntries: accessEntries,
|
AccessEntries: accessEntries,
|
||||||
ErrorEntries: errorEntries,
|
ErrorEntries: errorEntries,
|
||||||
Theme: flagTheme,
|
Theme: flagTheme,
|
||||||
MapAPIKey: flagMapAPIKey,
|
|
||||||
}
|
}
|
||||||
report.ComputeStats(&data)
|
report.ComputeStats(&data)
|
||||||
|
|
||||||
|
|
@ -216,7 +212,6 @@ func runSplitByDay(
|
||||||
for i := range days {
|
for i := range days {
|
||||||
days[i].Data.IndexFile = indexBase
|
days[i].Data.IndexFile = indexBase
|
||||||
days[i].Data.DayTitle = days[i].Date.Format("2006-01-02")
|
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
|
// Per-day geo: set Count from this day's access entries
|
||||||
if len(geoPoints) > 0 {
|
if len(geoPoints) > 0 {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -67,10 +66,6 @@ type ReportData struct {
|
||||||
// Theme selects the visual theme (see themes.go). Empty or unrecognized
|
// Theme selects the visual theme (see themes.go). Empty or unrecognized
|
||||||
// falls back to DefaultTheme.
|
// falls back to DefaultTheme.
|
||||||
Theme string
|
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.
|
// DaySummary is one row in the index page's daily breakdown table.
|
||||||
|
|
@ -343,20 +338,6 @@ type reportRenderData struct {
|
||||||
MarkerColor string
|
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.
|
// GenerateHTML renders the full HTML report from data.
|
||||||
func GenerateHTML(data ReportData) (string, error) {
|
func GenerateHTML(data ReportData) (string, error) {
|
||||||
th := themeFor(data.Theme)
|
th := themeFor(data.Theme)
|
||||||
|
|
@ -373,7 +354,7 @@ func GenerateHTML(data ReportData) (string, error) {
|
||||||
Chart3: th.Chart3,
|
Chart3: th.Chart3,
|
||||||
ChartWarn: th.ChartWarn,
|
ChartWarn: th.ChartWarn,
|
||||||
ChartErr: th.ChartErr,
|
ChartErr: th.ChartErr,
|
||||||
MapTileURL: tileURLWithKey(th.MapTileURL, data.MapAPIKey),
|
MapTileURL: th.MapTileURL,
|
||||||
MapAttribution: th.MapAttribution,
|
MapAttribution: th.MapAttribution,
|
||||||
MarkerColor: th.MarkerColor,
|
MarkerColor: th.MarkerColor,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -624,56 +624,3 @@ func TestGenerateHTML_DayTitleInHeader(t *testing.T) {
|
||||||
t.Error("expected DayTitle in report HTML")
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ var cactusTheme = themeAssets{
|
||||||
Chart3: "#e0956b",
|
Chart3: "#e0956b",
|
||||||
ChartWarn: "#d99a3d",
|
ChartWarn: "#d99a3d",
|
||||||
ChartErr: "#c96a5b",
|
ChartErr: "#c96a5b",
|
||||||
MapTileURL: "https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}{r}.png",
|
MapTileURL: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
||||||
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
||||||
MarkerColor: "#e0956b",
|
MarkerColor: "#e0956b",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ var cyberpunkTheme = themeAssets{
|
||||||
Chart3: "#ff00c8",
|
Chart3: "#ff00c8",
|
||||||
ChartWarn: "#ffcc00",
|
ChartWarn: "#ffcc00",
|
||||||
ChartErr: "#ff4444",
|
ChartErr: "#ff4444",
|
||||||
MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png",
|
MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
|
||||||
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
||||||
MarkerColor: "#ff00c8",
|
MarkerColor: "#ff00c8",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ var purplerainTheme = themeAssets{
|
||||||
Chart3: "#e879f9",
|
Chart3: "#e879f9",
|
||||||
ChartWarn: "#fbbf24",
|
ChartWarn: "#fbbf24",
|
||||||
ChartErr: "#fb7185",
|
ChartErr: "#fb7185",
|
||||||
MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png",
|
MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
|
||||||
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
MapAttribution: `© <a href="https://carto.com/">CARTO</a>`,
|
||||||
MarkerColor: "#a78bfa",
|
MarkerColor: "#a78bfa",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
Loading…
Reference in a new issue