v1.6.0.7 — the Web UI could start unable to read its own config - #84
Merged
Merged
Conversation
added 3 commits
September 12, 2026 13:00
A node that rebooted holding 2589 undelivered events reported 2953 of them in the heartbeat, and the number only converged as the spool drained. backlog_events is derived, not counted: pending bytes divided by the average event size. That average comes from written_events / written_bytes, which are per-process counters starting at zero — so a feeder that has just started has nothing to measure, and fell back to a hardcoded 280 against a real average nearer 320. The estimate was worst at exactly the moment the backlog was largest and the operator was watching it. The average is now seeded from the segments already on disk: one bounded read of the newest segment measures what this node's events actually cost. The live measurement takes over as soon as anything is written, and a failed sample returns None rather than the fallback so it is retried instead of cached for the life of the process. Measured against the 2589-event case: 7.5% high before, 0.3% after, and within 0.5% throughout the drain. Zero remains exact, which is the reading operators act on.
A fresh install left the settings panel refusing to load, and put nothing in the journal to say why. It repaired itself later only because `droneaware refresh` re-applies the same permission grant as a side effect, so the cause was never established — the evidence lived in a volatile journal and was gone by the time anyone looked. Rather than guess at which install step lost the grant, droneaware-web.service now enforces the access it needs on every start, via an ExecStartPre that runs as root before the unit drops to User=droneaware. That closes the class instead of one instance, and it is idempotent: a node in this state now fixes itself at the next boot. The silence was the more expensive half. web_ui now reports an unreadable config.env once per distinct fault — with the uid it is running as, the mode and owner it found, and the command that repairs it — and reports again when the file becomes readable, so a repair is visible too. Latched so a two-second status poll cannot turn it into a log flood. Also corrects a divergence found while tracing this: the CLI's install-webui path created the droneaware account without --user-group, the flag install.sh adds deliberately so the group does not depend on USERGROUPS_ENAB. The fix had been applied to one copy and not the other. Latent on stock Pi OS, which defaults that setting to yes; on a node where it is no, the group is absent, the grant returns quietly and the symptom is identical to the above.
…raft ASTM F3411's System message carries an absolute timestamp — uint32 at bytes 20-23, seconds since 2019-01-01, GPS-derived. Three decoders (wifi_feeder, ble_feeder and the server) were written to agree on this message's layout and all three stopped at byte 14, so the field arrived on every System message and was discarded. It is the only absolute time anywhere in the broadcast, and being GPS-derived it is right even when the node's clock is not. A Pi has no RTC; one that boots without a network restores a stale time and stamps everything it hears with it. A node did that on 2026-09-12 and backdated a flight by 9h48m. Nothing on the node noticed, because every age on the page was computed from the same wrong clock and so read correctly — the error was invisible precisely because it was consistent. Location/Vector cannot substitute: its timestamp is seconds-past-the-hour, so it is ambiguous modulo an hour and a ten-hour error appears there as a twelve-minute discrepancy. Each feeder now measures the median offset between the time aircraft broadcast and its own clock, and publishes it to its tmpfs state file. The Web UI shows "out of sync by 9h 48m" once past a minute, with the direction in the tooltip. This is the only such check available to an offline node: clock_synced answers "did NTP ever reach us", which on a node that booted with no network is false in a way an operator cannot act on. Measured, never acted on. A System message is an unauthenticated broadcast, so setting the clock from one would let anything able to transmit RID walk a node's clock — and the node would then mis-stamp its own spool and rate-limiter windows too. The median across several aircraft also means one hostile or broken transmitter cannot move the estimate. drone_time is written to the spool, which is the node's own flight history and feeds the replay, and stripped before the POST: the server decodes the same value from the raw hex it already receives, and an unrecognised key is not something to introduce into a working ingest path. The strip handles sub-messages inside a message pack, which is where most of these timestamps actually arrive. Both feeders keep their own copy of the decoder, as before, but tests now assert the two produce identical output for identical bytes including malformed input, and the forwarder harness asserts end to end that the field reaches the spool and never the wire. "Kept in sync with that file" is a comment, not a check, and it is what let this gap survive.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two defects found during field testing of v1.6.0.6 on a touchscreen node.
The Web UI could reach first boot unable to read its own config
A fresh install left the settings panel refusing to load, with nothing in the
journal to say why. It later repaired itself, but only because
droneaware refreshhappens to re-apply the same permission grant as a side effect — sothe cause was never established. The evidence was in a volatile journal and had
been overwritten by the time anyone looked.
Three candidates were eliminated with evidence rather than reasoning:
sed -ion the first-boot token write — tested against a real660 root:droneawarefile in a setgid directory; ownership and mode survive intactuseradd→_grant_webui_access→enableis correct,and the missing-group case already warns
Rather than offer a fourth guess,
droneaware-web.servicenow enforces theaccess it needs on every start, through an
ExecStartPrethat runs as rootbefore the unit drops to
User=droneaware. Idempotent, and it closes the classrather than an instance: a node in this state now repairs itself at the next
boot regardless of which step lost the grant.
The silence was the more expensive half of this bug.
web_uinow reports anunreadable
config.envonce per distinct fault — naming the uid it runs as,the mode and owner it found, and the command that fixes it — and reports again
on recovery, so a repair is visible too. Latched, so a two-second status poll
cannot turn it into a log flood.
A divergence found while tracing it
The CLI's
install-webuicreated thedroneawareaccount without--user-group— the flaginstall.shadds deliberately so that the groupdoes not depend on
USERGROUPS_ENABinlogin.defs. The fix had been appliedto one copy of the logic and not the other.
This is latent, not the bug above: stock Pi OS defaults that setting to
yes, so the group is created anyway. On a node where it isnothe group isabsent,
_grant_webui_accessreturns quietly, and the symptom is identical.The backlog count read high after a restart
A node that rebooted holding 2,589 undelivered events reported 2,953 of
them, converging only as the spool drained.
backlog_eventsis derived rather than counted — pending bytes over the averageevent size — and that average comes from per-process counters that start at
zero. A feeder that has just started has nothing to measure, so it fell back to
a hardcoded 280 against a real average nearer 320. The estimate was least
accurate at exactly the moment the backlog was largest.
The average is now seeded from the segments already on disk: one bounded read of
the newest segment measures what this node's events actually cost. A failed
sample returns
Nonerather than the fallback, so it is retried instead ofcached for the life of the process.
Within 0.5% throughout the drain, and zero stays exact — which is the reading
operators actually act on.
Validation
Unit validated with
systemd-analyze verify(exit 0). Spool suite 38/38,forwarder side-by-side harness against the shipping implementation 37/37, plus
12 new tests for the backlog estimate and 14 for the diagnostic logging.
Worth knowing
Deploying this via
droneaware refreshwill repair permissions as a sideeffect and mask the very thing being tested. To exercise the guard, break it
deliberately first —
chown root:rootandchmod 600on config.env, thenrestart the service and confirm it comes back as
root:droneaware 660.The node now says when its own clock is wrong
ASTM F3411's System message carries an absolute timestamp — uint32 at bytes
20–23, seconds since 2019-01-01, GPS-derived. Three decoders (both feeders and
the server) were written to agree on this message's layout and all three
stopped at byte 14, so the field arrived on every System message and was
thrown away. "Kept in sync with that file" is a comment, not a check — the
cross-check that should have caught this was comparing against a copy of the
same blind spot.
It matters because it is the only absolute time in the whole broadcast, and
being GPS-derived it is right when the node's clock is not. A Pi has no RTC; one
that boots with no network restores a stale time and stamps everything it hears
with it. A node did exactly that on 2026-09-12 and backdated a flight by
9h 48m. Nothing on screen said so, because every age was computed from the
same wrong clock and therefore read correctly — the error was invisible
because it was consistent.
Location/Vector cannot substitute: its timestamp is seconds-past-the-hour, so it
is ambiguous modulo an hour and a ten-hour error shows up there as a
twelve-minute discrepancy.
Each feeder now measures the median offset between broadcast time and its own
clock and publishes it to its tmpfs state file; the Web UI shows "out of sync
by 9h 48m" once past a minute. This is the only such check an offline node
has —
clock_syncedanswers "did NTP ever reach us", which on a node thatbooted without a network is false in a way an operator cannot act on.
Measured, never acted on. A System message is an unauthenticated broadcast,
so setting the clock from one would let anything able to transmit RID walk a
node's clock — and the node would then mis-stamp its own spool and rate-limiter
windows too. The median across several aircraft means one hostile or broken
transmitter cannot move the estimate.
drone_timeis written to the spool — the node's own history, which feedsthe replay — and stripped before the POST. The server decodes the same value
from the raw hex it already receives, so the field buys it nothing, and an
unrecognised key is not something to introduce into a working ingest path. The
strip handles sub-messages inside a message pack, which is where most of these
timestamps actually arrive.
Tests assert the two feeder copies produce identical output for identical bytes
including malformed input, and the forwarder harness asserts end to end that the
field reaches the spool and never the wire.