Backend (c2-core):
- app/internal/cameras/: provider interface, licence model, per-provider
catalog cache; one adapter per source in design-canvas/camera-sources.md.
Implemented: 511NY (legacy shape + tolerant XML inventory parser, 511x
endpoints UNVERIFIED), Caltrans, DelDOT, Ontario 511, DriveBC, Calgary,
TfL, NSW, Fintraffic. Stubs: NYC DOT, Westchester DPW, TRANSCOM, CTroads,
511NJ, Austin, TxDOT, MD CHART, WSDOT, Tallinn, Tarktee, Warendorf,
TrafficLand, Windy, EarthCam.
- Every provider defaults OFF (config/camera_providers). Gated licences
(agreement required / unverified / non-commercial / paid) need an explicit
acknowledgement stored with who/when and written to audit_log; the audit
write is a precondition. Stubs and prohibited sources cannot be enabled.
Ack is bound to the licence status it was given for.
- GET /cameras (signed-in), GET/PUT /admin/camera-providers (admin only).
- /traffic/511 follows the ny511 provider switch and returns the DAA credit
+ NYSDOT as-is disclaimer; 511NY event descriptions no longer truncated
("may not alter the substance of a report").
Frontend:
- LiveView becomes the ops wall: Region > Incident > Camera drill-down,
worst-first rail (MapView), KPIs, transcript ticker, scrubber incident
bands, auto-tour, deep links (#inc=&cam=&tour), Esc steps up.
- Incident panel: nearest cameras (path-weighted), fused situational context
(ADS-B, AIS, 511NY events, NEXRAD tile sample), units, call spine.
- MapView keeps every layer; adds Camera Viewsheds (approx.) overlay and a
nearest-camera layer for the selected incident.
- CameraFeed hygiene: hidden-tab/off-screen gating, 2 concurrent HLS cap,
last good frame + stale badge, frozen-frame signature, HLS stall watchdog,
provider attribution rendered on the imagery.
- Camera/event popups show source credit, update time and the disclaimer.
- Admin > Camera providers tab with licence status, health, gated confirm.
- Camera detection is not built (blocked on #199/#200); seam only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import type { CameraCredit } from "@/lib/types";
|
|
|
|
/** "3 min ago (14:02)" for an epoch-seconds catalog/feed fetch time. */
|
|
export function fetchedLabel(epochS: number | null | undefined): string | null {
|
|
if (!epochS) return null;
|
|
const ms = epochS * 1000;
|
|
const m = Math.round((Date.now() - ms) / 60_000);
|
|
const t = new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
return m < 1 ? `just now (${t})` : `${m} min ago (${t})`;
|
|
}
|
|
|
|
/**
|
|
* Source credit + freshness + pass-through disclaimer for third-party data
|
|
* (511NY DAA: credit as a source credit only, "display the time at which the
|
|
* data was published or last updated wherever you present it", and "best
|
|
* efforts" to pass on NYSDOT's as-is disclaimer). Render it wherever that
|
|
* provider's data or imagery is shown.
|
|
*/
|
|
export function SourceCredit({
|
|
credit,
|
|
updated,
|
|
fallbackName,
|
|
className,
|
|
}: {
|
|
credit?: CameraCredit | null;
|
|
/** Human-readable "updated …" text; required by the DAA wherever 511NY data shows. */
|
|
updated?: string | null;
|
|
fallbackName?: string;
|
|
className?: string;
|
|
}) {
|
|
const name = credit?.attribution ?? fallbackName ?? null;
|
|
return (
|
|
<div className={`text-[10px] text-ink-muted space-y-0.5 ${className ?? ""}`}>
|
|
<div>
|
|
{name && (credit?.attribution_url ? (
|
|
<a href={credit.attribution_url} target="_blank" rel="noopener noreferrer" className="underline hover:text-ink-2">{name}</a>
|
|
) : <span>{name}</span>)}
|
|
{name && updated ? " · " : ""}
|
|
{updated ? `updated ${updated}` : ""}
|
|
</div>
|
|
{credit?.disclaimer && (
|
|
<details>
|
|
<summary className="cursor-pointer select-none">Provided “as is”, without warranty — details</summary>
|
|
<p className="mt-0.5 leading-snug whitespace-normal">{credit.disclaimer}</p>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|