fix: say when the camera catalog serves only part of a region

Camera packs keep their nearest cameras and the catalog cap thins them
further, so London answered 250 cameras while TfL offers many more. The
catalog now records each pack's full count, tags cameras with their pack,
and serves the packs it trimmed; find_cctv_cameras names them.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:36 -07:00
1 parent e779432925
commit 66b07418bd
6 files changed
+117 -11

No files matched your search

+2
View File
@@ -143,8 +143,10 @@ export function cctvProxy({ sourceRoot = process.cwd() } = {}) {
if (url.pathname === '/sources') {
const body = {
trimmedPacks: getCctvSources.trimmedPacks?.() ?? [],
sources: sources.map((source) => ({
id: source.id,
pack: source.pack,
name: source.name,
city: source.city,
cityId: source.cityId,
+18 -1
View File
@@ -143,6 +143,9 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) {
/** @type {Promise<Array<object>>|null} In-flight refresh, shared by concurrent
* callers so a post-TTL burst launches ONE refetch, not one per request. */
let _cctvSourceInflight = null;
/** @type {Array<{pack: string, available: number, served: number}>} Packs
* that serve fewer cameras than they offer. */
let _cctvTrimmedPacks = [];
/**
* Assemble and cache the merged CCTV source list.
@@ -209,8 +212,11 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) {
name,
sources: items
.filter((item) => item && typeof item === 'object')
.map((item) => normalizeSourceItem(item))
.map((item) => ({ ...normalizeSourceItem(item), pack: name }))
.filter((item) => item.id),
available: Number.isFinite(items.available)
? items.available
: items.length,
});
const packs = [
...LIVE_PACKS.map((pack, index) =>
@@ -234,6 +240,14 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) {
loadGroundHeights(sourceRoot),
);
const trimmed = allocation.packs.filter((pack) => pack.kept < pack.offered);
// Each pack's own nearest-first cap and the shared catalog cap both trim.
const trimmedPacks = packs
.map((pack, index) => ({
pack: pack.name,
available: Math.max(pack.available, allocation.packs[index].offered),
served: allocation.packs[index].kept,
}))
.filter((pack) => pack.served < pack.available);
if (trimmed.length) {
const detail = trimmed
.map((pack) => `${pack.name} ${pack.kept}/${pack.offered}`)
@@ -244,6 +258,7 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) {
}
if (capped.length > 0 || _cctvSourceCache.length === 0) {
_cctvSourceCache = capped;
_cctvTrimmedPacks = trimmedPacks;
} else {
// Every source came back empty (all live packs timed out / upstream outage)
// but a good catalog is already cached — serve it stale rather than blanking
@@ -257,5 +272,7 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) {
return _cctvSourceCache;
}
/** Packs serving fewer cameras than they offer, as of the last refresh. */
getCctvSources.trimmedPacks = () => _cctvTrimmedPacks;
return getCctvSources;
}
+4 -1
View File
@@ -470,7 +470,10 @@ export function prioritizeSources(cameras, maxCount, anchors) {
return a.idx - b.idx;
});
return scored.slice(0, cap).map((entry) => entry.camera);
const kept = scored.slice(0, cap).map((entry) => entry.camera);
// How many cameras the pack offered before trimming, for coverage reports.
Object.defineProperty(kept, 'available', { value: list.length });
return kept;
}
/**