You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
First-run setup in one step, and the dashboard stays private until you open it
The first time you open Pingularity there's now a short Quick Setup: it asks how
often to run speed tests, who can reach the dashboard, whether to check for
updates, and optionally sets a login - then applies all of it at once. Nothing
starts probing your connection until you've said go, and you can change any of it
later in Settings.
The dashboard now starts private everywhere - it answers only the machine it's
running on until you decide otherwise. That includes Docker, which used to open
itself to the network by guessing at the setup; it no longer guesses. To reach
Pingularity from other devices, turn on network access in Settings or start it
with -access network (in a container, -e PINGULARITY_ACCESS=network), and set a
login. A container's published port stays closed until you do, so it can't be
exposed by accident.
Fixes that came with it:
- The browser's own login popup no longer appears on top of Pingularity's, and if
your session expires mid-download you're sent to the app's login, not the
browser's. "Redact PII" now downloads the masked log, not the raw one.
- Quick Setup works with a keyboard and a screen reader, and a login that pops up
over it is usable instead of stuck behind it.
- Saving one setting no longer quietly re-saves all the others (which could pin a
value you'd set on the command line).
- Automatic speed tests pick a server that actually answers instead of ranking an
unreachable one first, don't run twice right after startup, and read FreeBSD's
congestion-control list correctly.
- A backup too big for the browser now tells you to copy the database file or
fetch it with curl, instead of freezing the tab.
- Self-built container images stop warning about a data directory they never
touched, and a malformed release tag is refused instead of shipping the wrong
thing.
Co-Authored-By: Claude <noreply@anthropic.com>
# Reject build metadata here rather than silently mishandle it.
62
+
case "$tag" in
63
+
*+*)
64
+
echo "::error::tag '$tag' carries +build metadata, which this release pipeline does not support (it breaks the stable/prerelease split and Docker tagging); cut vMAJOR.MINOR.PATCH[-prerelease] instead"
65
+
exit 1
66
+
;;
67
+
esac
37
68
38
69
# Stable releases are immutable: their checksums are attested in the publish
39
70
# job, so silently overwriting a published stable asset would invalidate that
@@ -83,6 +114,19 @@ jobs:
83
114
with:
84
115
go-version-file: go.mod
85
116
117
+
# GoReleaser publishes the tag it resolves at HEAD (git describe --exact-match),
118
+
# not GITHUB_REF_NAME. The guard job validated the ref name; assert they are the
119
+
# same tag here, so a dispatch can never publish a DIFFERENT release than the one
Copy file name to clipboardExpand all lines: README.md
+58-19Lines changed: 58 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,13 +21,14 @@ go build -o pingularity . # requires Go 1.25.12+; pure Go, no cgo
21
21
./pingularity # probes every 5s; UI on http://localhost:9000
22
22
```
23
23
24
-
No flags needed. The UI binds `:9000` by default, but a fresh native install
25
-
starts **private**: a built-in filter answers only the machine it runs on, and
26
-
other devices get `403` until you flip **Network access** on in the settings
27
-
drawer's Access tab (flip it on and hit Save; the tab shows the address to use). Docker
28
-
installs skip the filter - it can't work behind container networking - so
29
-
there the dashboard is LAN-reachable immediately. `-listen 127.0.0.1:9000`
30
-
hard-pins it to local-only at the socket level.
24
+
No flags needed. The UI binds `:9000` by default, but every install starts
25
+
**private**: a built-in filter answers only the machine it runs on, and other
26
+
devices get `403` until you flip **Network access** on in the settings drawer's
27
+
Access tab (flip it on and hit Save; the tab shows the address to use), or start
28
+
with `-access network`. This is true in a container too - a published port
29
+
returns `403` until you set `-access network` (or `-e PINGULARITY_ACCESS=network`),
30
+
so a container is never accidentally exposed. `-listen 127.0.0.1:9000` hard-pins
31
+
it to local-only at the socket level regardless.
31
32
32
33
Connectivity is probed over **both IPv4 and IPv6** (each as an independent
33
34
quorum of three anycast anchors). IPv6 is auto-detected - skipped on IPv4-only
@@ -191,6 +192,14 @@ The image is multi-arch (amd64 + arm64). Two flags matter:
191
192
Exit row shows as unavailable (everything else still works). Spelling it out
192
193
keeps the command correct everywhere.
193
194
195
+
> **Reaching the dashboard from other devices.** Every install starts
196
+
> loopback-only, containers included - it is never guessed open from the network
197
+
> setup. With `--network=host` the dashboard answers on the *host's*
198
+
>`localhost:9000`, but other devices on your LAN get `403`until you opt in with
199
+
>`-access network` (or `-e PINGULARITY_ACCESS=network`) - set a login at the same
200
+
> time. A bridged container that publishes a port with `-p` needs the same flag,
201
+
> or the published port returns `403`.
202
+
194
203
The **`-v pingularity-data:/var/lib/pingularity`** volume is what makes updates
195
204
safe: the SQLite database *and*`pingularity.key` (which encrypts saved iperf3
196
205
passwords) live there. Skip the volume and a `docker pull` + recreate throws
@@ -623,8 +632,12 @@ and persist across restarts:
623
632
config / latency / speed / downtime, export them to a JSON file, and import one
624
633
back - time-series data is **merged** (existing/newer local rows are kept, only
625
634
missing rows are added) while**config is overwritten** and reloaded live.
626
-
Both ends stream, so even a multi-hundred-MB export of years of history
627
-
round-trips. The import warns you when it matters: restored rows older than
635
+
Both ends stream on the wire, but the *browser* download buffers the file in
636
+
memory, so a very large backup (years of dense history) can outgrow the tab -
637
+
the dashboard stops and says so rather than hanging. For one that big, copy the
638
+
SQLite database file at the `-db` path (that single file *is* the complete
639
+
backup), or stream `/api/export` straight to disk with `curl -OJ` (add
640
+
`-u user:pass` when a login is set). The import warns you when it matters: restored rows older than
628
641
your current retention windows will be pruned within the hour (raise
629
642
retention first to keep them), and a config restore that carried "login on"
630
643
without a password leaves login off until you set one.
@@ -640,15 +653,12 @@ and persist across restarts:
640
653
- **Access** → access controls (changes here apply on **Save**).
641
654
**Network access** decides whether other devices can reach the dashboard /
642
655
API / `/metrics`, or only this machine - a live loopback filter, so remote
643
-
clients get 403. It starts **off on native installs** (localhost-only until
644
-
you flip it) and **on in Docker**. In a *bridged* container the network hides
645
-
who a request really came from, so there the filter cannot be enforced -
646
-
publish the port narrowly and use the login password instead (the API reports
647
-
the difference as `local_only` vs `local_only_active`, and the tab says so). A
648
-
`--network=host` container sees real peer addresses, so local-only is enforced
649
-
there exactly as on a native install. The tab shows the **reachable
650
-
address(es)** with port plus a static-IP hint. **Require login** (off by
651
-
default) gates
656
+
clients get 403. It starts **off everywhere** (localhost-only until you flip
657
+
it), containers included: the loopback filter is enforced the same way in every
658
+
environment, and a container that must be reachable opts in explicitly with
659
+
`-access network` (or `-e PINGULARITY_ACCESS=network`) rather than being guessed
660
+
open. The tab shows the **reachable address(es)** with port plus a static-IP
661
+
hint. **Require login** (off by default) gates
652
662
everything behind a password: browsers get a login form + session cookie,
653
663
while API clients and Prometheus use HTTP Basic with the same credentials
654
664
(passwords are capped at 72 bytes, the bcrypt limit). Failed logins are
@@ -733,6 +743,7 @@ the settings drawer afterward and persists across restarts.
733
743
| Flag | Default | Purpose |
734
744
| --- | --- | --- |
735
745
| `-listen` | `:9000` | UI + metrics address (`127.0.0.1:9000` = local-only at the socket) |
746
+
| `-access` | `local` | who may open the dashboard: `local` (loopback only) or `network` (reachable from the LAN - set a login). A container that publishes a port needs `network` (or `PINGULARITY_ACCESS=network`), or the published port returns 403. Also settable in the UI |
| `-interval` | `5s` | time between probe rounds, `1s`-`1h` (a value saved in the UI takes precedence) |
738
749
| `-timeout` | `3s` | per-target dial timeout, `1s`-`30s` (a value saved in the UI takes precedence) |
@@ -747,10 +758,19 @@ the settings drawer afterward and persists across restarts.
747
758
| `-allow-host` | *(none)* | extra `Host` header values the DNS-rebinding guard accepts - only needed behind a reverse proxy on a public domain |
748
759
| `-trusted-proxy` | *(none)* | proxy IPs/CIDRs whose `X-Forwarded-For` identifies the real client, so one visitor's failed logins can't rate-limit everyone behind the proxy |
749
760
| `-metrics-token` | *(none)* | optional read-only token a scraper presents to `/metrics` (Bearer or Basic password) instead of the admin login, so Prometheus needn't hold an account that can change settings; only consulted when Require login is on |
761
+
|`-quick-setup`|`prompt`| headless first-run: `skip` starts monitoring immediately and never shows the browser Quick Setup dialog;`prompt` leaves it for a first visit |
750
762
751
763
Out-of-range numeric flags are rejected at startup (and at `pingularity
752
764
install`) rather than silently adjusted.
753
765
766
+
>**Headless installs:** a genuinely fresh install waits (monitoring paused) for
767
+
> a first-run consent - either the browser **Quick Setup** dialog or an explicit
768
+
> flag - so it never starts probing before someone has said to. Passing any
769
+
> monitoring flag (`-speedtest`, `-speedtest-interval`, `-latency`, `-interval`)
770
+
> counts as that consent;if you only tune other knobs (say `-timeout` or
771
+
>`-ipv6`) pass `-quick-setup=skip` so the service starts monitoring at boot
772
+
> instead of holding for the dialog.
773
+
754
774
## Metrics (optional)
755
775
756
776
>**Grafana users:** there is an official importable dashboard (latency
@@ -1064,7 +1084,11 @@ constant memory.
1064
1084
uptime to report, exactly as `pingularity_uptime_ratio` is then absent. A
1065
1085
running speedtest is reported as `speedtest_running` plus `speedtest_run_id`
1066
1086
(`0` when idle) - that id is what `/api/speedtest/abort` takes, so a stop can
1067
-
name the run it was decided against
1087
+
name the run it was decided against. A fresh install awaiting first-run consent
1088
+
reports `quick_setup_pending`, and `access_local_only` mirrors the loopback-only
1089
+
access filter (so a client can default the Quick Setup access choice to how the
1090
+
install booted);`bridged_container` is present only in a bridged container,
1091
+
where measurements describe the container network rather than the host's
1068
1092
- `GET /api/series?mins=…[&exclude=…]` - latency / online time series (server-side
1069
1093
bucketed); `exclude` drops targets from the lowest-latency line. Also takes an
0 commit comments