Files
authentik/packages/geo/README.md
Teffen Ellis 4e092df177 web, packages/geo, server/static: Air Gapped Maps Merge Branch (#24254)
* server/static: cover range requests for static assets

The Go web server this originally fixed is gone — `ak server` now execs the
Rust binary, and its static handler is a tower-http `ServeDir` behind a
compression layer. Both halves of the old fix are moot there: `ServeDir`
serves ranges itself, tower-http never compresses a response carrying
`Content-Range`, and no ETag middleware survived the rewrite.

Nothing to port, then, but the events map still byte-serves its PMTiles
basemap out of `/static/dist/`, so pin the behavior it depends on: a ranged
request comes back as an uncompressed 206 with an accurate `Content-Length`,
while a full request is still gzipped. The compression layer moves behind a
named constructor so the test exercises the same configuration the router
builds.

refs #21849

* brands: add branding_map_tiles for the events map tile source (#24253)

* brands: add branding_map_tiles for the events map tile source

Brand-level override for where the events map loads its vector tiles:
empty keeps the bundled basemap, a pmtiles:// archive URL or XYZ template
points at your own. Includes the migration, schema, and regenerated
clients.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* packages/geo: add @goauthentik/geo and the hexworld event map (#24255)

A standalone Lit + MapLibre package for the events map: a tilted globe
that bins events into H3 cells and raises them as action-colored pie
columns, over a hex basemap bundled as a PMTiles archive — no tile server
and no external requests, so it works air-gapped. Zoom bands cross-fade
and columns animate between re-bins. Ships the archive, the generator CLI,
and node tests for the geometry, styling, and tiling plan.

Co-authored-by: Teffen Ellis <teffen@Teffens-MacBook-Pro.local>

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@Teffens-MacBook-Pro.local>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* web: replace the OpenLayers events map with ak-map (#24257)

* brands: add branding_map_tiles for the events map tile source

Brand-level override for where the events map loads its vector tiles:
empty keeps the bundled basemap, a pmtiles:// archive URL or XYZ template
points at your own. Includes the migration, schema, and regenerated
clients.

* packages/geo: add @goauthentik/geo and the hexworld event map

A standalone Lit + MapLibre package for the events map: a tilted globe
that bins events into H3 cells and raises them as action-colored pie
columns, over a hex basemap bundled as a PMTiles archive — no tile server
and no external requests, so it works air-gapped. Zoom bands cross-fade
and columns animate between re-bins. Ships the archive, the generator CLI,
and node tests for the geometry, styling, and tiling plan.

* web: replace the OpenLayers events map with ak-map

EventMap now renders @goauthentik/geo's globe: events show as extruded
columns split by action, hovering shows a localized breakdown, and
clicking a column filters the list to that cell's events. The build copies
the bundled archive and glyphs into dist. OpenLayers and the pin-marker
path are removed.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@Teffens-MacBook-Pro.local>

* Format.

* brands: note that branding_map_tiles is served unauthenticated

The events map is admin-only, but branding_map_tiles rides along in
CurrentBrandSerializer, which /core/brands/current/ exposes with
AllowAny. Commercial tile providers carry their API key in the URL, and
the help text invites pasting exactly such a URL, so say plainly that
the value is world-readable.

Migration 0016 is edited in place rather than superseded — it has not
shipped, and makemigrations reports no pending changes. Schema and
clients regenerated.

* geo: strip build-machine paths from the shipped basemap archive

The committed hexworld.pmtiles carried the absolute path of the scratch
directory it was built in — including a session uuid — in its metadata
name, description and generator_options, shipped to every install.
tile-join inherits those fields from its first input file, so pass
--name/--description/--attribution explicitly to stop it recurring.

The archive itself is rewritten in place rather than regenerated:
pmtiles v3 lays out header, root directory, metadata, leaf directories
and tile data contiguously, and directory entries address tiles relative
to tileDataOffset, so resizing the metadata only shifts two header
offsets. Verified with the pmtiles reader — header fields match and 634
sampled tiles across z0-7 are byte-identical.

* web: drop the unused OpenLayers map pin

map_pin.svg was the marker icon the OpenLayers events map drew; ak-map
renders extruded columns instead and nothing references the file.

Also correct the preserveSymlinks comment. The flag is load-bearing, but
not for the stated reason: geo resolves its own dependencies fine from
its own node_modules. What it prevents is resolving them by realpath,
which pulls a second copy of the Lit runtime out of .pnpm alongside the
one in web/node_modules — two lit-html/lit-element/@lit-reactive-element
trees and two ReactiveElement hierarchies in one bundle.

* build: pin playwright through the pnpm catalog in both workspaces

The root and web/ are separate pnpm workspaces with separate lockfiles,
so a caret range let them resolve playwright independently — root landed
on 1.62.0 while web sat at 1.61.1, and `playwright install` downloads a
~95 MB browser build keyed to the exact version. Two versions, two
downloads. Catalog entries plus lint-catalogs turn that drift into a
failing check; the pin is exact because a range is what allowed it.

Held at 1.61.1 rather than the newest: 1.62.0 cannot resolve a bare
package name in a tsconfig `extends`, and web/tsconfig.json extends
"@goauthentik/tsconfig", so loading web's playwright.config.js fails and
the e2e suite never runs. `playwright test --list` discovers 25 tests on
1.61.1 and dies before discovery on 1.62.0.

vitest, vite and the @vitest/browser pair join the same catalog since
geo now uses them too and @vitest/browser-playwright drives whichever
playwright it finds.

* geo: rebuild ak-map on re-parent, run tests from source under vitest

disconnectedCallback tore the MapLibre instance down but firstUpdated
only ever fires once, so re-parenting <ak-map> left it permanently
blank. Rebuild from connectedCallback once the element has updated.
The new Chromium test covers exactly that: it fails without the fix and
passes with it, and no other test moves.

Tests move from node:test over compiled out/*.js to vitest over src, so
they exercise the source rather than a stale build and need no build
step. Six of them reached for ../out/*.js through a dynamic import and
would have kept asserting against whatever was last compiled.

Along the way, three things that were already broken:

  - `tsc -p scripts` never ran anywhere and does not compile — its
    tsconfig omits the DOM lib while its import graph reaches
    src/style.ts, which uses `window`. There is now a lint:types script
    covering src, scripts and test.
  - TippecanoeFeature extended a bare Feature though placeFeature
    always emits a Point with fixed properties.
  - The README described a previous generation of the generator: wrong
    script path, wrong cut names, a zoom band that stops at z7 rather
    than z8, a shipped archive listed at 8.8 MB when it is 22 MiB, and
    markers painted "via MapLibre feature-state" when they are a
    fill-extrusion source.

publishConfig is dropped rather than `private`: geo depends on
@goauthentik/api via link:, which cannot survive publication, so the
package is unpublishable either way and publishConfig was the dead half.

* rust nits

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* website/docs: document the hexworld event map

* website/docs: drop the OSM tile server from the air-gapped outbound list

The events map no longer reaches tile.openstreetmap.org — the bundled
basemap makes no outbound connections. Note the one way it can reach out
again: a custom basemap configured on a brand.

* Ignore build info.

* Ignore build info.

* Fix pins.

* Fix formatting.

* Fix duplicate package entries.

* Move runtime code to web.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Teffen Ellis <teffen@Teffens-MacBook-Pro.local>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-07-31 18:54:08 +00:00

5.4 KiB
Raw Blame History

@goauthentik/geo

Generator for the hexworld basemap archive that authentik's events map renders.

This package is build-time machinery only — nothing here ships to the browser. It turns Natural Earth vector data and a Protomaps planet dump into tiles/hexworld.pmtiles, the archive web copies into its bundle. The map element itself, and the styles that read this archive, live in web/src/elements/maps/.

The contract with the runtime

Three things must agree between the archive and the element that draws it, so the element owns them and this package imports them rather than redeclaring:

What Owned by
Zoom → H3 resolution bands web/src/elements/maps/bands.ts
Label kinds and reveal zooms web/src/elements/maps/labels.ts
Attribution string web/src/elements/maps/attribution.ts

All three are import-free for this reason — pulling them from the style module would drag MapLibre into a Node build script. The dependency runs one way only: tooling reads the app's contract, never the reverse.

Tests

pnpm run test         # Vitest, node only
pnpm run lint:types   # tsc over src, scripts and test

Covers the land-fill, border, country-assignment, detail-zone and label normalization math. The element's own tests live with the element, under web/test/unit/maps/ and web/test/component/.

Zoom bands

The hexworld archive baked at build time uses three H3 resolutions (HEX_BANDS in src/hexworld/bands.ts). Measured via h3's getHexagonEdgeLengthAvg and getHexagonAreaAvg — cell "width" here means vertex-to-opposite-vertex, roughly 2× the edge:

Zoom range H3 resolution Edge length Cell width Cell area
z0z2 3 ~69 km ~138 km ~12,393 km²
z3z6 4 ~26 km ~52 km ~1,770 km²
z7 5 ~10 km ~20 km ~253 km²

z7 is the archive's maxzoom; MapLibre overzooms beyond it, and bandForZoom clamps to the res-5 band so events keep binning at that resolution. The bands are baked into every published archive — changing HEX_BANDS invalidates existing tiles.

The shipped archive

tiles/hexworld.pmtiles is committed to the repo. The current file is the detail cut (~22 MiB): a full res-3/res-4 grid worldwide, plus a res-5 overlay restricted to the populated-area detail zone (see src/hexworld/detail-zone.ts).

tiles/fonts/ ships the Latin Noto Sans glyph ranges (Regular + Medium, 0-255 and 256-511) alongside the archive, under SIL Open Font License 1.1 (see tiles/fonts/OFL.txt). Both the archive and the glyphs are committed so the web build stays hermetic — no network required after clone.

The web build copies tiles/hexworld.pmtiles and tiles/fonts/ into web/dist/assets/maps/. If either is missing at build time the build fails loudly instead of silently shipping a broken map.

Regenerating the archive

The generator lives at scripts/build-hexworld.ts. It needs:

  • Node ≥ 24 and this workspace installed (pnpm install).
  • tippecanoe and go-pmtiles on PATH.
  • A local PMTiles planet dump extracted to z08 (a ~13 GB slice of a Protomaps planet build).
# Preview the shell pipeline without running it:
pnpm run hexworld:build -- --dry-run --out tiles

# Full run — emits both size cuts:
pnpm run hexworld:build -- --dump ./planet-z8.pmtiles --out tiles
# tiles/hexworld-detail.pmtiles  ← res 3 + 4 + zoned res 5  (shipped)
# tiles/hexworld-plain.pmtiles   ← res 3 + 4 only  (smaller, coarser)

Inputs the generator downloads on first run are pinned to specific releases so a re-run a year from now produces the same tiles:

  • Natural Earth vector data: nvkelso/natural-earth-vector@v5.1.2. ne_50m_land.geojson supplies the land polygons and ne_50m_admin_0_countries.geojson the country assignment; state and province borders come from ne_10m_admin_1_states_provinces.geojson since the 50m admin-1 dataset only covers nine countries.
  • Protomaps planet build: 20260521 — the source of the labels layer. The shipped archive was cut from that build; regenerate against a newer build to pick up new places.

The generator walks land, country, and region data into H3 cells, extracts labels from the dump's places layer via pmtiles + MVT decoders, computes border segments along shared cell edges wherever two adjacent cells differ (country borders take precedence over region borders at the same edge), then hands everything to tippecanoe and tile-join. Border features carry a level property so the runtime style can render country and region borders with different weights from the same source-layer. Cuts are always emitted together; size is a manual gate — pick whichever fits the ship budget after inspecting both in the pmtiles viewer.

To ship a regenerated archive, copy the chosen cut over the committed one:

AUTHENTIK_HEXWORLD_SOURCE=/path/to/hexworld-detail.pmtiles \
  pnpm run tiles:pull-hexworld
git add tiles/hexworld.pmtiles && git commit

Runtime override

A brand can point the events map at its own tile server (System > Brands > Map tiles), which bypasses this archive entirely — see web/src/elements/maps/basemap-style.ts.