mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-10 20:50:25 +02:00
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:
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user