Expose AISHub as an optional ships-layer backup when AISStream is silent.

Wire AISHUB_USERNAME into API Keys and onboarding, and surface backup status on the AIS outage banner so operators can keep vessel coverage without changing the AISStream proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-08-09 18:56:30 -06:00
co-authored by Cursor
parent 00f02c2687
commit dc0f69a0b9
12 changed files with 194 additions and 109 deletions
+1 -1
View File
@@ -993,7 +993,7 @@ export default function Dashboard() {
{/* AIS UPSTREAM OUTAGE BANNER — renders only when AIS is configured
but the WebSocket upstream is unreachable. Tells users the empty
ocean isn't their fault. */}
<AisUpstreamBanner />
<AisUpstreamBanner onOpenApiKeys={() => setSettingsOpen(true)} />
{/* ONBOARDING MODAL */}
{showOnboarding && (
+26 -10
View File
@@ -1,15 +1,18 @@
/**
* AisUpstreamBanner — visible notice that AIS ship data is unavailable
* because the upstream provider (AISStream) is offline.
* AisUpstreamBanner — visible notice that AISStream ship data is unavailable.
*
* Renders nothing when AIS is healthy or when AIS isn't configured at all.
* Mounted at the app shell level so users see it before they wonder why
* the ocean looks empty.
* When AISStream is silent, nudge operators toward the existing AISHub REST
* backup (Settings → API Keys) or confirm that backup is already active.
*/
import { useState } from 'react';
import { useAisUpstreamHealth } from '@/hooks/useAisUpstreamHealth';
export function AisUpstreamBanner() {
type AisUpstreamBannerProps = {
onOpenApiKeys?: () => void;
};
export function AisUpstreamBanner({ onOpenApiKeys }: AisUpstreamBannerProps = {}) {
const health = useAisUpstreamHealth();
const [dismissed, setDismissed] = useState(false);
@@ -29,6 +32,10 @@ export function AisUpstreamBanner() {
}
}
const detail = health.aishubConfigured
? `AISStream is silent (${stalenessLabel}). AISHub backup polling stays active on a slower cadence (~20 min) — live WebSocket traffic will resume when AISStream recovers.`
: `AISStream is silent (${stalenessLabel}). Add a free AISHub username under Settings → API Keys → Maritime for slow backup ship coverage while AISStream is down.`;
return (
<div
role="status"
@@ -38,12 +45,21 @@ export function AisUpstreamBanner() {
<div className="flex items-start gap-3">
<span aria-hidden className="mt-0.5 text-amber-300"></span>
<div className="flex-1">
<div className="font-semibold">Ship data temporarily unavailable</div>
<div className="text-xs opacity-90">
AISStream upstream is offline ({stalenessLabel}). The map will
refill once their service comes back online nothing is wrong
with your install.
<div className="font-semibold">
{health.aishubConfigured
? 'Live AIS offline — AISHub backup active'
: 'Ship data temporarily unavailable'}
</div>
<div className="text-xs opacity-90">{detail}</div>
{!health.aishubConfigured && onOpenApiKeys ? (
<button
type="button"
onClick={onOpenApiKeys}
className="mt-2 text-[11px] font-mono tracking-wide text-amber-100 underline underline-offset-2 hover:text-white"
>
Open API Keys
</button>
) : null}
</div>
<button
type="button"
+21 -3
View File
@@ -38,6 +38,20 @@ const API_GUIDES = [
url: 'https://aisstream.io/authenticate',
color: 'blue',
},
{
name: 'AISHub (backup)',
icon: <Ship size={14} className="text-blue-300" />,
required: false,
description:
'Slow REST backup for the ships layer when AISStream is silent or offline. Uses the same map layer on a ~20 minute cadence.',
steps: [
'Create a free account at aishub.net',
'Open the API page and note your username',
'Paste the username into Quick Local Setup above or Settings → API Keys → Maritime',
],
url: 'https://www.aishub.net/api',
color: 'blue',
},
{
name: 'Global Fishing Watch',
icon: <Ship size={14} className="text-teal-400" />,
@@ -79,6 +93,7 @@ const OnboardingModal = React.memo(function OnboardingModal({
OPENSKY_CLIENT_ID: '',
OPENSKY_CLIENT_SECRET: '',
AIS_API_KEY: '',
AISHUB_USERNAME: '',
GFW_API_TOKEN: '',
});
const [setupSaving, setSetupSaving] = useState(false);
@@ -129,6 +144,7 @@ const OnboardingModal = React.memo(function OnboardingModal({
OPENSKY_CLIENT_ID: '',
OPENSKY_CLIENT_SECRET: '',
AIS_API_KEY: '',
AISHUB_USERNAME: '',
GFW_API_TOKEN: '',
});
setSetupMsg({ type: 'ok', text: 'Keys saved locally. Restart or refresh feeds to use them.' });
@@ -577,9 +593,10 @@ const OnboardingModal = React.memo(function OnboardingModal({
</p>
<p className="text-sm text-[var(--text-secondary)] font-mono leading-relaxed">
OpenSky Network and AIS Stream are the free keys that make ShadowBroker
useful immediately: live aircraft and vessel tracking. Global Fishing Watch
unlocks the fishing-activity layer. Paste them below or use Settings later;
secrets stay on the local backend.
useful immediately: live aircraft and vessel tracking. Optionally add an
AISHub username as a slow ships-layer backup when AISStream is silent.
Global Fishing Watch unlocks the fishing-activity layer. Paste them below
or use Settings later; secrets stay on the local backend.
</p>
</div>
</div>
@@ -599,6 +616,7 @@ const OnboardingModal = React.memo(function OnboardingModal({
['OPENSKY_CLIENT_ID', 'OpenSky Client ID'],
['OPENSKY_CLIENT_SECRET', 'OpenSky Client Secret'],
['AIS_API_KEY', 'AIS Stream API Key'],
['AISHUB_USERNAME', 'AISHub Username (optional backup)'],
['GFW_API_TOKEN', 'Global Fishing Watch API Token (optional)'],
].map(([key, label]) => (
<input
@@ -8,6 +8,10 @@
* banner can explain "AIS upstream is offline" instead of letting users
* wonder.
*
* When AISStream is silent, the backend can still fill ships via AISHub REST
* (`AISHUB_USERNAME`) on a slow cadence. ``aishubConfigured`` tells the banner
* whether to nudge the operator to add that backup or confirm it is active.
*
* The poll interval is intentionally relaxed (30s) — this is a low-urgency UX
* signal, not a real-time data feed. Backend already escalates top_status to
* "degraded" when AIS is configured-but-disconnected.
@@ -35,6 +39,8 @@ export interface AisUpstreamHealth {
* seen — we approximate it by requiring at least one spawn before
* declaring an outage. */
aisEnabled: boolean;
/** True when ``AISHUB_USERNAME`` is set so the REST backup can run. */
aishubConfigured: boolean;
}
const POLL_INTERVAL_MS = 30_000;
@@ -67,6 +73,7 @@ export function useAisUpstreamHealth(): AisUpstreamHealth | null {
degradedTls: Boolean(proxy.degraded_tls),
proxySpawnCount: spawns,
aisEnabled: spawns > 0,
aishubConfigured: Boolean(proxy.aishub_configured),
});
} catch {
// Backend unreachable — separate problem. Banner not relevant.