A real-time, self-contained PHP dashboard for monitoring a GPS/PPS-disciplined Stratum 1 NTP server running on Linux. It interrogates ntpd, gpsd, the kernel clock, and PPS devices directly on each page load and renders status dashboard. No database, no JavaScript framework, no external dependencies.
This dashboard was built specifically to monitor the health and precision of a Stratum 1 NTP server that derives its time from a GPS receiver with a 1 PPS (pulse-per-second) output. It surfaces everything you need to know at a glance: NTP synchronization state, PPS lock status, GPS satellite fix quality, peer association health, kernel clock discipline metrics, and upcoming leap second events.
The page auto-refreshes every 10 seconds.
- NTP sync summary parsed from
ntpstatwith colored status badges (synced / unsynced / no server) - NTP peer table from
ntpq -pnwith tally codes, offset, jitter, reach, and association condition badges - Full
ntpq -c rvvariable dump with priority-ordered fields and light unit labels - PPS discipline card showing lock state, refid, offset, frequency, jitter, and wander
- GPS/GPSD status card from live
gpspipeJSON (TPV + SKY) and NMEA sentence streams: fix quality, satellite count, HDOP/PDOP/VDOP, lat/lon, altitude, speed, and UTC time - Kernel timekeeping block from
adjtimex --print: offset (µs), frequency (raw + estimated ppm), max error, estimated error, time constant, and status word - Leap second awareness: parses
leap-seconds.listfrom disk to show the current TAI-UTC offset and any scheduled future leap second - Leap indicator badge decoded from
ntpq -c rv(none / insert pending / delete pending / alarm) - Service status cards for both
ntpdandgpsdviasystemctlwithpsfallback for non-systemd hosts - PPS sample viewer via a 2-second
ppswatchcapture showing recent sequence numbers and nanosecond offsets - DST and stratum badges in the page header so the most critical state is immediately visible
- GPS year rollover warning when NMEA sentences report an implausible year (common with old receivers that have a rollover bug)
- Responsive layout: two-column cards collapse to single-column on narrow screens
All executable commands are defined in a single allowed_commands() array. The run_cmd() function accepts only a string key from that map; it is impossible to execute an arbitrary command through this interface. The PPS device path, the only runtime-variable component of any command, is passed through escapeshellarg().
Each daemon output is handled by a dedicated parser:
| Function | Input | Output |
|---|---|---|
parse_ntpstat() |
ntpstat text |
sync state, stratum, server, offset, poll interval |
parse_ntpq_peers() |
ntpq -pn table |
array of peer rows with tally code |
parse_ntpq_rv() |
ntpq -c rv blob |
status flags array + key/value map |
parse_ntpq_as() |
ntpq -c as table |
association rows with condition fields |
parse_gpspipe_json() |
gpspipe -w JSON stream |
latest TPV and SKY objects |
parse_nmea_stream() |
gpspipe -r NMEA stream |
decoded GGA / GSA / RMC / VTG / ZDA / GLL fields |
parse_ppswatch() |
ppswatch output |
sequence numbers and nanosecond offsets |
parse_adjtimex_print() |
adjtimex --print text |
kernel clock fields (offset, freq, errors, etc.) |
parse_leap_seconds_list() |
file path | sorted NTP-epoch entries with TAI offset |
Additional helpers:
ntp_hex_to_unix_seconds()— converts NTP hexadecimal timestamps (e.g.reftime) to Unix epoch, enabling human-readable "last sync age" displayadjtimex_freq_ppm_estimate()— converts the raw kernel frequency integer to a parts-per-million estimate (divides by 65536 per Linux kernel convention)leap_info_from_list()— walks the leap-seconds.list entries to determine the current TAI-UTC offset and format any upcoming leap second eventbadge_class_for()/leap_badge()/assoc_condition_badge_class()— map state strings to CSS badge classes (ok/warn/bad)nmea_degmin_to_decimal()— converts NMEA degree-minute notation to signed decimal degreesnmea_checksum_ok()— validates NMEA sentence XOR checksums
| Card | Source | Key Metrics |
|---|---|---|
| NTP Time | ntpq -c rv, adjtimex, leap-seconds.list, system date |
Current time, UTC offset, ISO-8601, epoch, stratum/refid, last reftime age, leap schedule, kernel clock fields |
| NTP Sync Summary | ntpstat |
Sync state, upstream server, stratum, offset, poll interval |
| PPS Discipline | ntpq -c rv flags + gpspipe NMEA |
PPS lock state, refid, stratum, offset, frequency, sys/clk jitter, clk wander |
| GPSD Status | gpspipe -w JSON + gpspipe -r NMEA |
Fix type, satellite count, HDOP/PDOP/VDOP, lat/lon, altitude, speed, track, NMEA UTC time |
| Services | systemctl + ps fallback |
Active state and full status output for ntpd and gpsd |
| NTP Peers | ntpq -pn merged with ntpq -c as |
All peers with tally code, refid, stratum, reach, delay, offset, jitter, association condition |
| NTPD Variables | ntpq -c rv |
Full key/value dump, priority-sorted, with unit labels |
- Linux (tested on RHEL/CentOS 7+; compatible with any systemd-based distro)
- A GPS receiver attached via serial or USB, outputting NMEA sentences
- A 1 PPS signal wired to a PPS-capable serial port or GPIO, exposed as a
/dev/pps*device via thepps-ldiscorpps-gpiokernel module ntpd(NTP Reference Implementation, notchronyorsystemd-timesyncd)gpsdwithgpspipeavailableppswatchutilityadjtimexcommand-line toolsystemctl(orpsas fallback for SysV init)
- PHP 5.4 or later (no composer dependencies; no extensions beyond the standard
pcreand file I/O functions) shell_exec()must not be disabled inphp.ini
- Any standard PHP-capable web server
- The PHP worker process must have execute permission for the binaries listed in
allowed_commands()inlib.php
Open lib.php and edit the two constants near the top:
$GLOBALS['PPS_DEVICE'] = '/dev/pps1'; // path to your PPS device
$GLOBALS['PPS_SAMPLES'] = 3; // number of ppswatch lines to retainThe auto-refresh interval is set in index.php:
$refresh = 10; // secondsIf any binary path differs on your system (e.g. ntpq lives at /usr/bin/ntpq instead of /usr/sbin/ntpq), update the corresponding entry in the allowed_commands() array in lib.php.
- No user input reaches the shell. Every command is looked up by a static key; the only dynamic value (
PPS_DEVICE) is escaped withescapeshellarg(). - All rendered output is HTML-escaped via
htmlspecialchars()before being written to the page. - No authentication is included. This dashboard is intended for deployment on a private or firewalled network. Do not expose it to the public internet without adding an authentication layer (HTTP Basic Auth, a reverse-proxy auth gateway, etc.).
shell_exec()is required. If your hosting environment disablesshell_exec, the dashboard will not function. This is expected behavior for a system monitoring tool running on a dedicated server.
This project is released under the MIT License.