CodingNic

Feature Build-Out

Directing Multi-Region Compare & Saved Locations

Feature Build-Out 30 min read

Directing Multi-Region Compare & Saved Locations

Objectives

By the end of this chapter, you should be able to:

  • Direct a side-by-side compare view that reuses positionsByRegion instead of standing up any new backend, and verify the AI actually noticed that data was already there
  • Prompt a region-scoped map pane that reuses MapPanel’s pieces instead of duplicating them, and catch the specific stale-map bug that shows up when it doesn’t
  • Direct a saved-locations bookmark feature and verify, concretely, that saving a bookmark never touches what the poller watches

💡 Why this matters: This is the lesson where “does the AI actually understand the architecture, or is it just generating plausible code” gets tested hardest. Two watch regions on screen at once and a list of saved map points look, from a UI screenshot, almost identical. They are not the same feature, they don’t share a backend, and an AI assistant that doesn’t already understand the difference between “what the poller tracks” and “where the user likes to look” will happily blur the line between them if you let it.

Say the Distinction Before You Prompt Either Feature

The poller has re-polled every enabled watch region independently since Module 2. The store has kept positionsByRegion, one entry per region, since Module 4. Put those two facts together and a compare view needs zero new backend – it’s a second consumer of data that’s already flowing. That’s worth stating explicitly in the prompt, because an AI assistant that doesn’t already know positionsByRegion exists might reasonably invent a new endpoint or a new poll cycle to solve a problem the codebase already solved.

Saved locations is the opposite kind of feature, and the prompt needs to say that just as plainly: a watch region is something the poller continuously tracks. A saved location is a bookmark for the frontend map view only – a name, a lat/lon, a zoom level – with zero effect on what’s being polled. The two features end up looking almost identical in a screenshot (a floating panel, a list, a name field, a jump-to action) which is exactly the trap. State the distinction up front, or you’ll be reviewing code that quietly wires “save this bookmark” into the same table the poller reads from.

The Prompt

code
Two features for SKYWATCH, and I want to be explicit about how they're different before you write either one. FEATURE 1 -- Multi-region compare. Since Module 2 the poller re-polls every enabled watch region independently, and since Module 4 the store keeps positionsByRegion (Record<number, AircraftState[]>), one entry per region. A side-by-side compare view needs NO new backend work -- it's a second, region-scoped consumer of data that's already flowing. Build: - CompareView: two map panes side by side, each with its own region dropdown (populated from useWatchRegions) and its own live aircraft count. Default to two different enabled regions if at least two exist, otherwise fall back to whatever's available, including picking the same region twice if only one exists. The two dropdowns are independent -- changing one must not affect the other. - RegionMapPane: a lean, region-scoped map. No free-pan, no history playback, no watch-region CRUD -- reuse radiusNmToZoom and makePlaneIcon from the existing lib/ code and MapPanel rather than reimplementing them. It reads aircraft directly from positionsByRegion[region.id], not from whatever the primary map's "current view" logic resolves to. Watch out for one specific react-leaflet gotcha: MapContainer doesn't re-point itself at a new center/zoom prop after the initial mount, so when the user picks a different region in a dropdown, the pane needs to actually remount with the new region's center, not silently keep showing the old framing with new markers on top of it. Also show a banner when the region is disabled, since a disabled region still has a lat/lon to render a pane at but its positionsByRegion entry is frozen at whatever it last held. - Wire a compare-mode toggle into the header and Dashboard, swapping the entire main content area (map + detail panel + flight list) for CompareView while active, not layering it on top. FEATURE 2 -- Saved locations. This is NOT a watch region and must not touch anything the poller reads. It's a frontend-only bookmark: a name, lat, lon, zoom level. Jumping to one should behave exactly like the user panned there by hand -- it needs to flow into our existing ViewportFeed free-pan handling and fetch live traffic on demand, the same as any manual pan. Do not mark it as a programmatic move the way our watch-region "recenter" jump does (that one deliberately hides itself from free-pan detection -- this one deliberately should not). Build: - A saved_locations repo (list/create/delete, no partial unique index, no "is this the default" logic -- there's no invariant to enforce beyond "rows have a name and a place") and three REST routes. - SavedLocationsControl: a floating map panel, list of saved spots, name input + save button capturing the current map center/zoom, delete per row. Structurally similar to WatchRegionsControl is fine (same "small floating CRUD panel" shape), but its jumpTo function must NOT call whatever function marks a move as programmatic -- a saved-location jump should trigger the same free-pan behavior as a manual drag, not the watch-region recenter behavior. Show me every file for both features.

What It Built

CompareView, defaulting to two enabled regions where possible:

tsx
// apps/web/src/components/CompareView.tsx
"use client";

import { useEffect, useState } from "react";
import type { AircraftState } from "@skywatch/shared";
import { useAppStore } from "@/store/useAppStore";
import { useWatchRegions } from "@/hooks/useWatchRegions";
import { RegionMapPane } from "./RegionMapPane";

const EMPTY_AIRCRAFT: AircraftState[] = [];

function RegionSelect({
  value,
  onChange,
  options,
}: {
  value: number | null;
  onChange: (id: number) => void;
  options: { id: number; name: string; enabled: boolean }[];
}) {
  return (
    <select
      value={value ?? ""}
      onChange={(e) => onChange(Number(e.target.value))}
      className="min-w-0 flex-1 truncate rounded-sm border border-line bg-surface-raised px-2 py-1.5 font-mono text-[11px] text-fg focus:outline-none"
    >
      {options.length === 0 && <option value="">No watch regions yet</option>}
      {options.map((r) => (
        <option key={r.id} value={r.id}>
          {r.name}
          {r.enabled ? "" : " (disabled)"}
        </option>
      ))}
    </select>
  );
}

export function CompareView() {
  const { regions, loaded } = useWatchRegions();
  const [leftId, setLeftId] = useState<number | null>(null);
  const [rightId, setRightId] = useState<number | null>(null);

  useEffect(() => {
    if (!loaded || regions.length === 0 || leftId != null) return;
    const enabled = regions.filter((r) => r.enabled);
    const pool = enabled.length > 0 ? enabled : regions;
    setLeftId(pool[0].id);
    setRightId(pool.length > 1 ? pool[1].id : pool[0].id);
  }, [loaded, regions, leftId]);

  const leftAircraft = useAppStore((s) => (leftId != null ? (s.positionsByRegion[leftId] ?? EMPTY_AIRCRAFT) : EMPTY_AIRCRAFT));
  const rightAircraft = useAppStore((s) =>
    rightId != null ? (s.positionsByRegion[rightId] ?? EMPTY_AIRCRAFT) : EMPTY_AIRCRAFT
  );

  const leftRegion = regions.find((r) => r.id === leftId) ?? null;
  const rightRegion = regions.find((r) => r.id === rightId) ?? null;

  if (loaded && regions.length === 0) {
    return (
      <div className="flex h-full items-center justify-center px-8 text-center text-[12px] tracking-[0.05em] text-fg-dim">
        No watch regions configured yet -- add at least two from Watch Regions on the map to compare them side by
        side.
      </div>
    );
  }

  return (
    <div className="flex h-full min-w-0 flex-1">
      <div className="flex min-w-0 flex-1 flex-col border-r border-line">
        <div className="flex items-center gap-2 border-b border-line bg-surface px-2.5 py-2">
          <RegionSelect value={leftId} onChange={setLeftId} options={regions} />
          <span className="shrink-0 font-mono text-[10px] text-fg-dim">{leftAircraft.length} ac</span>
        </div>
        <div className="relative min-h-0 flex-1">
          {leftRegion && <RegionMapPane region={leftRegion} aircraft={leftAircraft} />}
        </div>
      </div>

      <div className="flex min-w-0 flex-1 flex-col">
        <div className="flex items-center gap-2 border-b border-line bg-surface px-2.5 py-2">
          <RegionSelect value={rightId} onChange={setRightId} options={regions} />
          <span className="shrink-0 font-mono text-[10px] text-fg-dim">{rightAircraft.length} ac</span>
        </div>
        <div className="relative min-h-0 flex-1">
          {rightRegion && <RegionMapPane region={rightRegion} aircraft={rightAircraft} />}
        </div>
      </div>
    </div>
  );
}

RegionMapPane, with the key={region.id} remount fix in place:

tsx
// apps/web/src/components/RegionMapPane.tsx
"use client";

import { memo, useMemo } from "react";
import { MapContainer, Marker, TileLayer } from "react-leaflet";
import type { AircraftState, WatchRegion } from "@skywatch/shared";
import { altitudeFt, isOnGround } from "@skywatch/shared";
import { useAppStore } from "@/store/useAppStore";
import { useThemeStore } from "@/store/useThemeStore";
import { makePlaneIcon } from "@/lib/aircraftIcon";
import { radiusNmToZoom } from "@/lib/mapView";

const DARK_TILES = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
const LIGHT_TILES = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
const TILE_ATTRIBUTION = "&copy; OpenStreetMap &copy; CARTO";

interface PaneMarkerProps {
  aircraft: AircraftState & { lat: number; lon: number };
  isSelected: boolean;
  onSelect: (hex: string) => void;
}

const PaneMarker = memo(function PaneMarker({ aircraft, isSelected, onSelect }: PaneMarkerProps) {
  const track = aircraft.track ?? aircraft.true_heading ?? aircraft.mag_heading ?? 0;
  const altFt = altitudeFt(aircraft);
  const onGround = isOnGround(aircraft);
  const icon = useMemo(
    () => makePlaneIcon(track, isSelected, altFt, onGround),
    [track, isSelected, altFt, onGround]
  );

  return (
    <Marker
      position={[aircraft.lat, aircraft.lon]}
      icon={icon}
      zIndexOffset={isSelected ? 1000 : 0}
      eventHandlers={{ click: () => onSelect(aircraft.hex) }}
    />
  );
});

export const RegionMapPane = memo(function RegionMapPane({
  region,
  aircraft,
}: {
  region: WatchRegion;
  aircraft: AircraftState[];
}) {
  const isDark = useThemeStore((s) => s.isDark);
  const selectedIcao = useAppStore((s) => s.selectedIcao);
  const setSelectedIcao = useAppStore((s) => s.setSelectedIcao);

  const center: [number, number] = [region.lat, region.lon];
  const zoom = radiusNmToZoom(region.radiusNm);

  const withPosition = aircraft.filter(
    (a): a is typeof a & { lat: number; lon: number } => a.lat != null && a.lon != null
  );

  return (
    <div className="relative h-full w-full">
      <MapContainer
        key={region.id}
        center={center}
        zoom={zoom}
        minZoom={2}
        maxZoom={12}
        worldCopyJump
        zoomControl
        attributionControl={false}
        className="h-full w-full"
      >
        <TileLayer
          key={isDark ? "dark" : "light"}
          url={isDark ? DARK_TILES : LIGHT_TILES}
          attribution={TILE_ATTRIBUTION}
          subdomains="abcd"
          maxZoom={19}
        />
        {withPosition.map((a) => (
          <PaneMarker key={a.hex} aircraft={a} isSelected={a.hex === selectedIcao} onSelect={setSelectedIcao} />
        ))}
      </MapContainer>

      {!region.enabled && (
        <div className="absolute inset-x-3 top-3 z-[500] rounded-sm border border-line bg-surface/95 px-2.5 py-1.5 text-center text-[10px] tracking-[0.05em] text-fg-dim">
          This region is disabled -- enable it in Watch Regions to see live traffic here.
        </div>
      )}
    </div>
  );
});

Wiring compare mode into the header and dashboard:

tsx
// apps/web/src/components/Header.tsx (additions)
export interface HeaderProps {
  // ...existing props...
  compareMode: boolean;
  onToggleCompareMode: () => void;
}

// in the icon row:
<button
  type="button"
  onClick={onToggleCompareMode}
  aria-pressed={compareMode}
  title={compareMode ? "Exit compare mode" : "Compare two watch regions side by side"}
  className={`flex h-7 w-7 items-center justify-center rounded-sm border transition-colors ${
    compareMode
      ? "border-phosphor-dim text-phosphor"
      : "border-line text-text-dim hover:border-phosphor-dim hover:text-phosphor"
  }`}
>
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
    <rect x="3" y="4" width="18" height="16" rx="1.5" />
    <line x1="12" y1="4" x2="12" y2="20" />
  </svg>
</button>
tsx
// apps/web/src/components/Dashboard.tsx (additions)
import dynamic from "next/dynamic";

const CompareView = dynamic(() => import("./CompareView").then((m) => m.CompareView), {
  ssr: false,
  loading: () => (
    <div className="flex h-full items-center justify-center text-xs tracking-[0.1em] text-fg-dim">
      LOADING COMPARE VIEW…
    </div>
  ),
});

// inside Dashboard():
const [compareMode, setCompareMode] = useState(false);

// ...
<Header
  // ...existing props...
  compareMode={compareMode}
  onToggleCompareMode={() => setCompareMode((v) => !v)}
/>

{compareMode ? (
  <div className="flex min-h-0 flex-1">
    <CompareView />
  </div>
) : (
  <div className="flex min-h-0 flex-1">
    {/* MapPanel + detail panel + flight list, unchanged */}
  </div>
)}

The saved-locations type, tacked onto the same file HomeLocationSettings already lives in:

ts
// packages/shared/src/location.ts (add to the Module 2 version)

/** A user-saved map location for the "jump to a favorite spot" dropdown. */
export interface SavedLocation {
  id: number;
  name: string;
  lat: number;
  lon: number;
  zoom: number;
  createdAt: string;
}

The saved-locations repo, deliberately with no invariants to protect:

ts
// apps/server/src/db/repos/savedLocationsRepo.ts
import { eq } from "drizzle-orm";
import type { SavedLocation } from "@skywatch/shared";
import { db } from "../client.js";
import { savedLocations } from "../schema.js";

function toApi(row: typeof savedLocations.$inferSelect): SavedLocation {
  return {
    id: row.id,
    name: row.name,
    lat: row.lat,
    lon: row.lon,
    zoom: row.zoom,
    createdAt: row.createdAt.toISOString(),
  };
}

export async function listSavedLocations(): Promise<SavedLocation[]> {
  const rows = await db.select().from(savedLocations).orderBy(savedLocations.createdAt);
  return rows.map(toApi);
}

export async function createSavedLocation(input: {
  name: string;
  lat: number;
  lon: number;
  zoom: number;
}): Promise<SavedLocation> {
  const [row] = await db.insert(savedLocations).values(input).returning();
  return toApi(row);
}

export async function deleteSavedLocation(id: number): Promise<boolean> {
  const deleted = await db.delete(savedLocations).where(eq(savedLocations.id, id)).returning();
  return deleted.length > 0;
}
ts
// apps/server/src/routes/savedLocations.ts
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import {
  createSavedLocation,
  deleteSavedLocation,
  listSavedLocations,
} from "../db/repos/savedLocationsRepo.js";

const postBodySchema = z.object({
  name: z.string().trim().min(1).max(80),
  lat: z.number().min(-90).max(90),
  lon: z.number().min(-180).max(180),
  zoom: z.number().min(1).max(19).default(9),
});
const deleteParamsSchema = z.object({ id: z.coerce.number().int().positive() });

export function registerSavedLocationsRoutes(app: FastifyInstance): void {
  app.get("/saved-locations", async () => {
    const locations = await listSavedLocations();
    return { locations };
  });

  app.post("/saved-locations", async (req, reply) => {
    const parsed = postBodySchema.safeParse(req.body);
    if (!parsed.success) {
      return reply.status(400).send({ error: "invalid saved location", issues: parsed.error.issues });
    }
    const location = await createSavedLocation(parsed.data);
    return reply.status(201).send({ location });
  });

  app.delete("/saved-locations/:id", async (req, reply) => {
    const parsed = deleteParamsSchema.safeParse(req.params);
    if (!parsed.success) return reply.status(400).send({ error: "invalid id" });
    const deleted = await deleteSavedLocation(parsed.data.id);
    if (!deleted) return reply.status(404).send({ error: "not found" });
    return reply.status(204).send();
  });
}

SavedLocationsControl, whose jumpTo deliberately does not mark the move as programmatic:

tsx
// apps/web/src/components/SavedLocationsControl.tsx
"use client";

import { useEffect, useState } from "react";
import { useMap } from "react-leaflet";
import type { SavedLocation } from "@skywatch/shared";
import { apiUrl } from "@/lib/serverUrl";

export function SavedLocationsControl() {
  const map = useMap();
  const [open, setOpen] = useState(false);
  const [locations, setLocations] = useState<SavedLocation[]>([]);
  const [loaded, setLoaded] = useState(false);
  const [newName, setNewName] = useState("");
  const [saving, setSaving] = useState(false);

  const refresh = () => {
    fetch(apiUrl("/api/saved-locations"))
      .then((r) => r.json())
      .then((data: { locations: SavedLocation[] }) => setLocations(data.locations ?? []))
      .catch((err) => console.error("[saved-locations] fetch failed:", err))
      .finally(() => setLoaded(true));
  };

  useEffect(() => {
    if (open && !loaded) refresh();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  const jumpTo = (loc: SavedLocation) => {
    map.setView([loc.lat, loc.lon], loc.zoom);
    setOpen(false);
  };

  const saveCurrentView = async () => {
    const name = newName.trim();
    if (!name) return;
    setSaving(true);
    try {
      const center = map.getCenter();
      const res = await fetch(apiUrl("/api/saved-locations"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, lat: center.lat, lon: center.lng, zoom: map.getZoom() }),
      });
      if (!res.ok) throw new Error(`save failed (${res.status})`);
      const data: { location: SavedLocation } = await res.json();
      setLocations((prev) => [...prev, data.location]);
      setNewName("");
    } catch (err) {
      console.error("[saved-locations] save failed:", err);
    } finally {
      setSaving(false);
    }
  };

  const remove = async (id: number, e: React.MouseEvent) => {
    e.stopPropagation();
    setLocations((prev) => prev.filter((l) => l.id !== id));
    try {
      await fetch(apiUrl(`/api/saved-locations/${id}`), { method: "DELETE" });
    } catch (err) {
      console.error("[saved-locations] delete failed:", err);
      refresh();
    }
  };

  return (
    <div className="absolute bottom-3.5 right-3.5 z-[500]">
      {open ? (
        <div className="w-[230px] rounded-sm border border-line bg-surface/95 p-3 text-[11px] text-fg">
          <div className="mb-2 flex items-center justify-between">
            <span className="text-[10px] uppercase tracking-[0.08em] text-fg-dim">Saved locations</span>
            <button type="button" onClick={() => setOpen(false)} className="text-fg-dim hover:text-fg">
              ✕
            </button>
          </div>

          <div className="mb-2 max-h-[180px] overflow-y-auto">
            {locations.length === 0 && (
              <div className="py-1.5 text-fg-dim">No saved locations yet.</div>
            )}
            {locations.map((loc) => (
              <div
                key={loc.id}
                role="button"
                tabIndex={0}
                onClick={() => jumpTo(loc)}
                onKeyDown={(e) => e.key === "Enter" && jumpTo(loc)}
                className="flex cursor-pointer items-center justify-between rounded-sm px-1 py-1.5 hover:bg-phosphor/10"
              >
                <span className="truncate text-fg">{loc.name}</span>
                <button
                  type="button"
                  onClick={(e) => remove(loc.id, e)}
                  className="ml-2 shrink-0 text-fg-dim hover:text-danger"
                  aria-label={`Delete ${loc.name}`}
                >
                  ✕
                </button>
              </div>
            ))}
          </div>

          <div className="flex gap-1.5 border-t border-line pt-2">
            <input
              value={newName}
              onChange={(e) => setNewName(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && saveCurrentView()}
              placeholder="Name this view…"
              className="min-w-0 flex-1 rounded-sm border border-line bg-surface-raised px-1.5 py-1 font-mono text-[11px] text-fg placeholder:text-fg-dim focus:outline-none"
            />
            <button
              type="button"
              onClick={saveCurrentView}
              disabled={saving || !newName.trim()}
              className="shrink-0 rounded-sm border border-phosphor-dim px-2 py-1 text-[10px] tracking-[0.05em] text-phosphor hover:bg-phosphor/10 disabled:opacity-50"
            >
              SAVE
            </button>
          </div>
        </div>
      ) : (
        <button
          type="button"
          onClick={() => setOpen(true)}
          className="rounded-sm border border-line bg-surface/90 px-3 py-2 text-[11px] tracking-[0.08em] text-phosphor transition-colors hover:border-phosphor-dim"
        >
          ★ SAVED LOCATIONS
        </button>
      )}
    </div>
  );
}

Review This

Did RegionMapPane actually get key={region.id} on its MapContainer, or does switching a dropdown leave a stale map behind? react-leaflet doesn’t re-point an already-mounted map at a new center/zoom prop after the fact – this is a real limitation, not a bug you can code around inside the component’s render logic, the only fix is forcing React to tear down and remount. An AI assistant that isn’t specifically warned about this will often write RegionMapPane exactly the way you’d naively expect a React component to work: pass region as a prop, let center/zoom update reactively. It compiles, and it even looks right the very first time you pick a region, because that’s the initial mount. It breaks the second time you change either dropdown, when the map pane keeps its old framing with new markers plotted on top of the wrong background. Check for key={region.id} on the MapContainer specifically. If it’s missing: “RegionMapPane’s MapContainer needs key={region.id} – react-leaflet won’t re-center an already-mounted map when center/zoom props change, so switching regions needs to force a remount, not rely on prop updates.”

Does SavedLocationsControl.jumpTo accidentally call whatever marks a move as programmatic? This is the mistake most worth catching in this entire lesson, because it’s the one place an AI assistant might reach for code reuse in exactly the wrong spot. WatchRegionsControl’s own jump-to-region function deliberately hides its move from free-pan detection, and if the AI pattern-matches “this looks like the same kind of jump” and copies that call over, saved locations silently stop behaving like a manual pan – the “RETURN TO WATCH REGION” free-pan control simply never appears, and nothing on screen tells you why. It still visually jumps to the right spot, so a five-second test looks completely fine. Check jumpTo in SavedLocationsControl for any call to a “mark as programmatic move” helper. If it’s there: “SavedLocationsControl.jumpTo must not mark the move as programmatic – a saved-location jump should trigger the same free-pan/ViewportFeed behavior as a manual drag, not the watch-region recenter behavior. Remove that call.”

Did the saved-locations repo pick up an isDefault-style flag or unique-index logic it doesn’t need? Because SavedLocationsControl looks structurally so similar to WatchRegionsControl, an AI assistant working from that similarity as a template can sometimes carry over more than the shape – an enabled column, a “set as default” route, a partial unique index that has no invariant behind it here. It’s harmless in the sense that nothing breaks, but it’s dead weight that misrepresents what this feature actually guarantees, and it’s exactly the kind of thing that looks like careful engineering in a schema review until you ask what it’s protecting. Check the saved_locations table and its repo for anything beyond plain CRUD. If something extra snuck in: “saved_locations doesn’t need an enabled flag, a default flag, or any unique index beyond the primary key – there’s no invariant to protect here beyond ‘rows have a name and a place.’ Strip out anything copied over from the watch-regions pattern that isn’t doing real work.”

Try It

  1. Restart the server so registerSavedLocationsRoutes is live, then reload the frontend.
  2. Make sure at least two watch regions exist. Click the compare-mode icon in the header and confirm two map panes appear, each with its own region dropdown and live aircraft count.
  3. Change the left pane’s dropdown to a different region and confirm only that pane’s map re-centers and re-populates – watch closely for a frame where the old tiles are still showing under new markers, which is exactly what a missing remount key looks like.
  4. Disable one of the two compared regions from Watch Regions, return to compare mode, and confirm that pane shows the “region is disabled” banner instead of stale traffic.
  5. Pan somewhere interesting, click ★ SAVED LOCATIONS, save that view under a name. Pan elsewhere, click the saved entry, and confirm the map jumps back – and confirm the free-pan “RETURN TO WATCH REGION” control actually appears. If it doesn’t appear, the jump was wrongly marked as programmatic.

Recap

  • CompareView and RegionMapPane needed zero new backend work – positionsByRegion, keyed by region since Module 4, already held everything two simultaneous panes needed. Worth confirming the AI actually noticed that instead of inventing a redundant endpoint.
  • The key={region.id} remount is a one-line fix for a real react-leaflet limitation, and it’s invisible until the second time someone switches a dropdown – exactly the kind of thing a first pass through the UI won’t catch.
  • Saved locations and watch regions solve different problems that happen to share a UI shape. The one line of code that actually enforces that difference is the absence of a “mark as programmatic” call in jumpTo – worth checking by name, not just by clicking around.
  • When two features share a UI shape, an AI assistant copying one as a template for the other can carry over more than the shape. Check what invariants actually got copied, not just what the buttons look like.

Next lesson: snapshot sharing and the mobile layout.