diff --git a/ONBOARDING.md b/ONBOARDING.md
index 23cf8990..8e8bff3b 100644
--- a/ONBOARDING.md
+++ b/ONBOARDING.md
@@ -3,9 +3,9 @@
Welcome. This repo powers RETINA, a passive-radar system: a network of receiver
nodes detect aircraft by listening to reflections of broadcast transmitters
(bistatic radar), and the backend turns those detections into tracks and live
-positions shown on a web map. It started life as "Tower Finder" (a tool to find
-suitable broadcast illuminators near a receiver), which is still one of the
-surfaces.
+positions shown on a web map. It started life as "Tower Finder", a tool to find
+suitable broadcast illuminators near a receiver; that feature now lives entirely
+in tower-finder-service, both its API and its UI.
Read this top-to-bottom once; it should get you from a fresh clone to running
the whole thing locally and understanding how the pieces fit. For deeper dives,
@@ -21,7 +21,11 @@ One FastAPI backend serves several React front-ends, distinguished by subdomain:
| **testmap** | Live aircraft map fed by the simulation fleet (synthetic nodes) — the main dev/demo surface. `testmap.retina.fm` is served by the **staging** droplet, the only environment still running a fleet. |
| **map** | Production live map showing only real radar nodes. |
| **dashboard** | Admin app (auth required): node ownership, claim codes, MLAT verification, metrics. |
-| **Tower Finder** | The original illuminator search. The SPA is here; the `/api/towers`, `/api/elevation` and `/api/config` API is **tower-finder-service** (separate repo and container), which nginx proxies to on every vhost. This backend no longer implements it. |
+
+Illuminator search is deliberately absent from that table: **tower-finder-service**
+(separate repo and container) owns the API and the UI both, and serves
+`towers.retina.fm` from its own edge. Our vhosts only proxy `/api/towers`,
+`/api/elevation` and `/api/config` to it.
Receiver nodes connect over TCP and stream detection frames. The pipeline
(tracker → geolocator) turns frames into aircraft positions, broadcast to the
diff --git a/README.md b/README.md
index 3dff08b6..de0fe2c1 100644
--- a/README.md
+++ b/README.md
@@ -2,25 +2,25 @@
This repo powers RETINA, a passive-radar system: receiver nodes detect aircraft
from reflections of broadcast transmitters, and the backend turns those
-detections into live tracks shown on a web map. "Tower Finder" — the original
-illuminator-search feature — is one of several surfaces (along with the live map
-and the admin dashboard), and is the one whose API now lives in its own service.
+detections into live tracks shown on a web map. Its surfaces are the live map
+and the admin dashboard.
> **New here?** Start with [`ONBOARDING.md`](ONBOARDING.md) for the full picture
> and local setup, and [`docs/architecture.md`](docs/architecture.md) for how the
> pieces fit together.
-## Tower Finder feature
+## Illuminator search
-Web application that helps passive radar operators find suitable broadcast tower
-illuminators near their location: given coordinates, it returns nearby FM/VHF/UHF
-transmitters ranked by suitability for passive radar use.
+Finding a suitable broadcast illuminator near a receiver is **tower-finder-service**,
+a separate repo and container. It owns both halves: the API (`/api/towers`,
+`/api/elevation`, `/api/config`) and the UI, which it serves itself on
+`towers.retina.fm` through its own edge.
-This repo owns the SPA (`frontend/`) and the nginx routing. The search API itself
-(`/api/towers`, plus `/api/elevation` and `/api/config`) is served by
-**tower-finder-service**, a separate repo and container that every vhost is
-proxied to; the monolith's own copy of that stack was deleted once the proxy went
-live, so there is one implementation and one ranking answer.
+What remains here is the proxy seam. `api.retina.fm/towers` forwards to the
+service for callers that want a clean public API name, and the other vhosts
+still forward `/api/towers`, `/api/elevation` and `/api/config` so that a
+request arriving at one of them reaches the single implementation rather than a
+404 from this backend.
## Project Structure
diff --git a/docs/architecture.md b/docs/architecture.md
index 61df84ba..6d918937 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -12,9 +12,9 @@ only by subdomain, resolved client-side in `frontend/src/utils/domains.ts`:
staging and local stacks run a fleet, so `testmap.retina.fm` is served by the
staging droplet rather than production.
- **map** (`map.retina.fm`) — production live map, real radar nodes only.
-- **Tower Finder** — `/api/towers` illuminator search (the original feature).
- The SPA is this repo's; the API is proxied to tower-finder-service, which owns
- the only implementation since the monolith's copy was deleted.
+- **Illuminator search** — not a surface of this repo. tower-finder-service owns
+ both the API and the UI, and serves `towers.retina.fm` from its own edge. The
+ vhosts here proxy `/api/towers`, `/api/elevation` and `/api/config` to it.
- **dashboard** (`dashboard/`, separate SPA) — admin: node ownership, claim
codes, MLAT verification, metrics. Auth required.
diff --git a/frontend/e2e/live-map.spec.ts b/frontend/e2e/live-map.spec.ts
index 4c4727c9..8274e0ea 100644
--- a/frontend/e2e/live-map.spec.ts
+++ b/frontend/e2e/live-map.spec.ts
@@ -103,8 +103,8 @@ async function rowsOrSkip(page: Page) {
test.describe("Live Map — page identity", () => {
test("page title contains RETINA", async ({ page }) => {
await page.goto(BASE);
- // The HTML
is static "Tower Finder" for all domains;
- // the domain identity is exposed in the h1 element instead.
+ // Asserted on the h1 rather than the HTML , which is static across
+ // every domain this bundle serves.
await expect(page.locator("h1")).toContainText(/RETINA/i);
});
diff --git a/frontend/index.html b/frontend/index.html
index cf1462d2..0bc4525b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,7 +3,7 @@
- Tower Finder
+ RETINA
t.distance_class === "Ideal").length;
- const bands = [...new Set(towers.map((t) => t.band))];
- const best = towers[0];
-
- return (
-
-
- {towers.length}
- Towers Found
-
-
- {ideal}
- Ideal Range
-
-
- {bands.join(", ")}
- Bands
-
- {best && (
-
- {best.callsign || "—"}
- Top Pick — {best.distance_km} km
-
- No suitable broadcast towers found within 80 km.
-
- )}
- >
- )}
-
- {/* Live map: mounted once ever, hidden when inactive to preserve WebSocket state */}
+ {/* Hidden rather than unmounted when inactive, to preserve WebSocket state */}
Loading map…
- );
-}
diff --git a/frontend/src/map-surface.css b/frontend/src/map-surface.css
index 5e02d3d3..9cb19cae 100644
--- a/frontend/src/map-surface.css
+++ b/frontend/src/map-surface.css
@@ -1,9 +1,7 @@
/**
* Map surface theme — dash.retina.fm's design system, applied to the live map.
*
- * Everything is scoped to `.app.map-surface`, a class App only sets on map
- * hostnames (see utils/domains.ts). Tower Finder shares this bundle and keeps
- * its own look untouched.
+ * Everything is scoped to `.app.map-surface`, the class App sets on the root.
*
* The names are dash's (dashboard/src/App.css), and the light block's values
* are too. Duplicated rather than shared because the two apps build separately,
diff --git a/frontend/src/test/App.test.tsx b/frontend/src/test/App.test.tsx
index f98ad905..99ebb7df 100644
--- a/frontend/src/test/App.test.tsx
+++ b/frontend/src/test/App.test.tsx
@@ -5,12 +5,11 @@ import App from "../App";
describe("App", () => {
it("renders the header", () => {
render();
- expect(screen.getByText("Tower Finder")).toBeInTheDocument();
+ expect(screen.getByText("RETINA")).toBeInTheDocument();
});
- it("shows the search form by default", () => {
+ it("opens on the live radar tab", () => {
render();
- // The search tab should be active, showing the search form
- expect(document.querySelector(".app")).toBeTruthy();
+ expect(document.querySelector(".app-body.live-active")).toBeTruthy();
});
});
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 78afd3d3..101bd7ce 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -4,27 +4,6 @@
import type { ArcBufferEntry } from "./components/map/arcBuffer";
-/** Single tower returned by /api/towers */
-export interface Tower {
- callsign: string | null;
- frequency_mhz: number;
- frequency_matched: boolean;
- band: string;
- distance_km: number;
- distance_class: string;
-}
-
-/** /api/towers response */
-export interface TowerSearchResponse {
- towers: Tower[];
- query: { lat: number; lon: number };
-}
-
-/** /api/elevation response */
-export interface ElevationResponse {
- elevation_m: number;
-}
-
/* ---- Aircraft / live feed ---- */
export interface Aircraft {
diff --git a/frontend/src/utils/domains.ts b/frontend/src/utils/domains.ts
index c9459073..80b3a5da 100644
--- a/frontend/src/utils/domains.ts
+++ b/frontend/src/utils/domains.ts
@@ -5,7 +5,7 @@
* only by subdomain. Each predicate here captures one concrete decision:
*
* isMapDomain: any "map" surface, on any environment. Used to
- * default to the Live Radar tab and hide tower search.
+ * derive the public-demo predicates below.
* usesRealOnlyFeed: hits /ws/aircraft/live so the synthetic fleet never
* appears, even if a node leaks through a bad filter.
* defaultsGroundTruthOff: ADS-B ground truth starts hidden.