Permit hourly checks for direct/routed repeaters. Closes #226.

This commit is contained in:
Jack Kingsman
2026-04-27 09:35:45 -07:00
parent 25041e1367
commit 047d713003
13 changed files with 697 additions and 34 deletions
@@ -221,16 +221,16 @@ export function TelemetryHistoryPane({
via the repeater pane, API calls to the endpoint (
<code className="text-[0.6875rem]">POST /api/contacts/&lt;key&gt;/repeater/status</code>
), or when the repeater is opted into interval telemetry polling, in which case the
repeater will be polled for metrics every 8 hours. You can see which repeaters are opted
into this flow in the{' '}
repeater will be polled for metrics automatically. Fetch frequency can be configured in{' '}
<a
href="#settings/database"
className="underline text-primary hover:text-primary/80 transition-colors"
>
Database &amp; Messaging
</a>{' '}
settings pane. A maximum of {MAX_TRACKED} repeaters may be opted into this for the sake
of keeping mesh congestion reasonable.
Settings &rarr; Database &amp; Messaging
</a>
, where you can also see which repeaters are currently opted in. A maximum of{' '}
{MAX_TRACKED} repeaters may be opted into this for the sake of keeping mesh congestion
reasonable.
</p>
{isTracked ? (
@@ -259,7 +259,7 @@ export function TelemetryHistoryPane({
disabled={toggling}
className="border-green-600/50 text-green-600 hover:bg-green-600/10"
>
{toggling ? 'Updating...' : 'Opt Repeater into 8hr Interval Metrics Tracking'}
{toggling ? 'Updating...' : 'Opt Repeater into Interval Metrics Tracking'}
</Button>
)}
</div>
@@ -92,7 +92,11 @@ export function SettingsDatabaseSection({
return () => {
cancelled = true;
};
}, [trackedTelemetryRepeaters.length, appSettings.telemetry_interval_hours]);
}, [
trackedTelemetryRepeaters.length,
appSettings.telemetry_interval_hours,
appSettings.telemetry_routed_hourly,
]);
useEffect(() => {
if (trackedTelemetryRepeaters.length === 0 || telemetryFetchedRef.current) return;
@@ -346,13 +350,41 @@ export function SettingsDatabaseSection({
restored if you drop back to a supported count.
</p>
)}
{schedule?.next_run_at != null && (
<p className="text-xs text-muted-foreground">
Next run at {formatTime(schedule.next_run_at)} (UTC top of hour).
</p>
)}
</div>
{/* Routed hourly toggle */}
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={appSettings.telemetry_routed_hourly}
onChange={() => {
const next = !appSettings.telemetry_routed_hourly;
void persistAppSettings({ telemetry_routed_hourly: next }, () => {});
}}
className="w-4 h-4 rounded border-input accent-primary mt-0.5"
/>
<div>
<span className="text-sm">Poll direct/routed-path repeaters hourly</span>
<p className="text-[0.8125rem] text-muted-foreground">
When enabled, tracked repeaters with a direct or routed path (not flood) are polled
every hour instead of on the scheduled interval above. Flood-only repeaters still
follow the normal schedule.
</p>
</div>
</label>
{schedule?.next_run_at != null && (
<p className="text-xs text-muted-foreground">
{schedule.routed_hourly ? 'Next flood run at' : 'Next run at'}{' '}
{formatTime(schedule.next_run_at)} (UTC top of hour).
</p>
)}
{schedule?.next_routed_run_at != null && (
<p className="text-xs text-muted-foreground">
Next direct/routed run at {formatTime(schedule.next_routed_run_at)} (UTC top of hour).
</p>
)}
{trackedTelemetryRepeaters.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
No repeaters are being tracked. Enable tracking from a repeater's dashboard.
@@ -362,6 +394,21 @@ export function SettingsDatabaseSection({
{trackedTelemetryRepeaters.map((key) => {
const contact = contacts.find((c) => c.public_key === key);
const displayName = contact?.name ?? key.slice(0, 12);
const routeSource = contact?.effective_route_source ?? 'flood';
// A forced-flood override (path_len < 0) still reports source
// "override", but the actual route is flood. Check the real path.
const hasRealPath =
contact?.effective_route != null && contact.effective_route.path_len >= 0;
const routeLabel = !hasRealPath
? 'flood'
: routeSource === 'override'
? 'routed'
: routeSource === 'direct'
? 'direct'
: 'flood';
const routeColor = hasRealPath
? 'text-primary bg-primary/10'
: 'text-muted-foreground bg-muted';
const snap = latestTelemetry[key];
const d = snap?.data;
return (
@@ -369,9 +416,16 @@ export function SettingsDatabaseSection({
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<span className="text-sm truncate block">{displayName}</span>
<span className="text-[0.625rem] text-muted-foreground font-mono">
{key.slice(0, 12)}
</span>
<div className="flex items-center gap-1.5">
<span className="text-[0.625rem] text-muted-foreground font-mono">
{key.slice(0, 12)}
</span>
<span
className={`text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded font-medium ${routeColor}`}
>
{routeLabel}
</span>
</div>
</div>
{onToggleTrackedTelemetry && (
<Button