@camstack/ui-library 1.2.191 → 1.2.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ import { DayWindow, DensityBucket } from '../cap-settings/recording-timeline-model.js';
2
+ export interface ClipBandBarProps {
3
+ /** The calendar day the window is inside. */
4
+ readonly day: Date;
5
+ readonly onDayChange: (day: Date) => void;
6
+ /** The visible window — the band, whenever no bucket is picked. */
7
+ readonly view: DayWindow;
8
+ /** The full day, the clamp for every zoom and pan. */
9
+ readonly dayBounds: DayWindow;
10
+ readonly onViewChange: (view: DayWindow) => void;
11
+ readonly buckets: readonly DensityBucket[];
12
+ readonly selectedBucketIndex: number | null;
13
+ readonly onSelectBucket: (index: number) => void;
14
+ readonly densityLoading: boolean;
15
+ }
16
+ export declare function ClipBandBar({ day, onDayChange, view, dayBounds, onViewChange, buckets, selectedBucketIndex, onSelectBucket, densityLoading, }: ClipBandBarProps): import("react").JSX.Element;
@@ -0,0 +1,58 @@
1
+ import { DayWindow } from '../cap-settings/recording-timeline-model.js';
2
+ /** The half-open instant range `listClips` is asked for. */
3
+ export interface ClipBand {
4
+ readonly fromMs: number;
5
+ readonly toMs: number;
6
+ }
7
+ /**
8
+ * How many local days a band may touch and still be asked for.
9
+ *
10
+ * Two, which is every band the timebar can produce: its view window never
11
+ * spans more than one calendar day, and the only way to reach a second is to
12
+ * sit across local midnight. A third is not a gesture this control has, so it
13
+ * is a bug or a future widening — either way the list refuses rather than
14
+ * guesses.
15
+ */
16
+ export declare const MAX_BAND_LOCAL_DAYS = 2;
17
+ /** What the surface may do with a band, decided once. */
18
+ export type ClipBandPlan = {
19
+ readonly kind: 'askable';
20
+ readonly band: ClipBand;
21
+ readonly localDays: number;
22
+ } | {
23
+ readonly kind: 'too-wide';
24
+ readonly band: ClipBand;
25
+ readonly localDays: number;
26
+ readonly maxLocalDays: number;
27
+ };
28
+ /**
29
+ * How many LOCAL calendar days the half-open band touches.
30
+ *
31
+ * The end instant is exclusive, so a band that ends exactly at the next local
32
+ * midnight is one day, not two — otherwise every whole-day band would report
33
+ * double what it costs.
34
+ */
35
+ export declare function localDaysInBand(band: ClipBand): number;
36
+ /** Whether this band may be asked for, and what it costs, in one value. */
37
+ export declare function planClipBand(band: ClipBand): ClipBandPlan;
38
+ /** A density bucket's own window, as `EventHeatmap` hands it back. */
39
+ export interface ClipBandBucket {
40
+ readonly startMs: number;
41
+ readonly endMs: number;
42
+ }
43
+ /**
44
+ * The band for the current selection: the picked bucket, or — when nothing is
45
+ * picked — the whole visible window.
46
+ *
47
+ * "Nothing picked means everything visible" is safe here and only here,
48
+ * because the visible window is itself bounded by the timebar. It is never
49
+ * "everything the camera holds" (D382's unstated ask).
50
+ */
51
+ export declare function bandForSelection(view: DayWindow, bucket: ClipBandBucket | null): ClipBand;
52
+ /**
53
+ * The band, spelled out.
54
+ *
55
+ * Every state renders it — an empty list has to say WHICH period it is empty
56
+ * for, the same rule the day label carried before the band replaced it.
57
+ */
58
+ export declare function formatBandLabel(band: ClipBand): string;
@@ -1,37 +1,36 @@
1
1
  /**
2
- * Day navigation for the clips browser — and the window `listClips` is asked
3
- * for.
2
+ * LOCAL calendar-day arithmetic for the clips browser.
4
3
  *
5
- * Every recording timestamp on the wire is the CAMERA's wall clock (D549
6
- * decision 7), and the operator navigates a calendar, so a "day" here is a
7
- * LOCAL wall-clock day: from the instant local midnight starts to the instant
8
- * the next local midnight starts, half-open. Two consequences that are pinned
9
- * by spec because both are invisible until they are wrong:
4
+ * The browser no longer NAVIGATES days — it follows a band selected on the
5
+ * recording timebar (D564) — but the local day survived the move as the unit
6
+ * the COST is counted in: a Reolink search is clamped to one camera-local day
7
+ * per call, so `clip-band.ts` counts a band's local days to decide whether it
8
+ * may be asked for. The window builder this file used to own went with the day
9
+ * picker; `recording-timeline-model.ts`'s `dayWindow(date, nowMs)` is the one
10
+ * that builds windows now, and one directory holding two functions of that
11
+ * name was the confusion worth deleting.
10
12
  *
11
- * - Building the window in UTC shifts every list by the browser's offset, and
12
- * an operator in Berlin would see a 22:00 clip filed under the next day.
13
+ * Two properties are pinned by spec because both are invisible until they are
14
+ * wrong:
15
+ *
16
+ * - Deriving the day in UTC shifts every band by the browser's offset, and an
17
+ * operator in Berlin would see a 22:00 clip filed under the next day.
13
18
  * - Walking days by adding 86 400 000 ms repeats or skips a calendar day
14
19
  * across a DST change. The walk goes through `Date`'s own day arithmetic.
15
20
  *
16
- * A day id is `YYYY-MM-DD` — the shape a URL and a `<input type="date">` both
17
- * take, so navigation never needs a second representation.
21
+ * A day id is `YYYY-MM-DD` — it orders lexicographically, which is what lets
22
+ * the band's day walk terminate on a comparison rather than on a count.
18
23
  */
19
- /** The half-open instant range `listClips` is asked for. */
20
- export interface ClipDayWindow {
21
- readonly since: number;
22
- readonly until: number;
23
- }
24
24
  /** The LOCAL calendar day an instant falls in, as `YYYY-MM-DD`. */
25
25
  export declare function toDayId(instant: Date): string;
26
- /** The half-open local-wall-clock window of one day. */
27
- export declare function dayWindow(dayId: string): ClipDayWindow;
28
26
  /** Walk `days` calendar days from `dayId` — DST-safe, because `Date` does it. */
29
27
  export declare function shiftDay(dayId: string, days: number): string;
30
28
  /**
31
- * The day, spelled out for the header.
29
+ * The day, spelled out — the calendar half of a band's label.
32
30
  *
33
- * An empty list has to say WHICH day it is empty for — "no clips" with no date
34
- * beside it is the ambiguity this surface exists to remove — so this label is
35
- * rendered next to every state, not only the populated one.
31
+ * An empty list has to say WHICH period it is empty for; "no clips" with no
32
+ * date beside it is the ambiguity this surface exists to remove. `formatBandLabel`
33
+ * puts the times around it, and every state renders the result, not only the
34
+ * populated one.
36
35
  */
37
36
  export declare function formatDayLabel(dayId: string): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * When a tile comes back for a still the server said was queued — and when it
3
+ * gives up (D563).
4
+ *
5
+ * ## Why a retry, and not a queue or a push
6
+ *
7
+ * A deferred refusal is FREE. The provider's lane rejects past its capacity
8
+ * without touching the camera, so asking again costs one cheap HTTP round trip
9
+ * to the addon and zero Baichuan commands. That is what rules out the
10
+ * alternatives: a push would need a channel, a subscription and server-side
11
+ * fan-out of 60 tile identities to save requests that cost nothing, and a
12
+ * client-side queue would be a second copy of the authority that already lives
13
+ * where the camera's real constraint is. Scroll-into-view alone does not help
14
+ * either — the 43 tiles measured refusing on 592 are already on screen.
15
+ *
16
+ * ## The shape
17
+ *
18
+ * Exponential, jittered, bounded. Exponential because a fixed interval either
19
+ * hammers or dawdles; JITTERED because 43 tiles refused in the same paint
20
+ * would otherwise come back in the same instant and refuse together again;
21
+ * BOUNDED because a tile that asks for ever is the other way to show the
22
+ * operator nothing.
23
+ *
24
+ * The server's `retry-after` is taken only when it is LONGER than the curve.
25
+ * The curve is the floor, so a provider answering `retry-after: 1` for a whole
26
+ * page cannot turn this into 43 requests a second.
27
+ *
28
+ * Sized against the measurement: one page is 60 tiles and the provider serves
29
+ * one mint in flight per host, so the last tile is ~60 mints away. The budget
30
+ * below outlasts that; a shorter one would be theatre.
31
+ */
32
+ /** How many times a tile comes back before it says so and stops. */
33
+ export declare const STILL_RETRY_ATTEMPTS = 4;
34
+ /** First wait, doubling from there. */
35
+ export declare const STILL_RETRY_BASE_MS = 4000;
36
+ /** No single wait is longer than this, whatever the server hints. */
37
+ export declare const STILL_RETRY_MAX_WAIT_MS = 30000;
38
+ export interface StillRetryInput {
39
+ /** 0 for the first retry, 1 for the second, … */
40
+ readonly attempt: number;
41
+ /** The server's `retry-after`, in ms, or `null` when it sent none. */
42
+ readonly serverHintMs: number | null;
43
+ /** Injected so the schedule is assertable; `Math.random` in production. */
44
+ readonly random: () => number;
45
+ }
46
+ export interface StillRetryPlan {
47
+ readonly waitMs: number;
48
+ }
49
+ /** The next wait, or `null` when the budget is spent. */
50
+ export declare function planStillRetry(input: StillRetryInput): StillRetryPlan | null;
@@ -1,11 +1,19 @@
1
1
  import { Clip } from '@camstack/types';
2
- /** The four things a tile can be. */
2
+ /** The five things a tile can be. */
3
3
  export type ClipTileMedia = {
4
4
  readonly kind: 'vouched';
5
5
  readonly url: string;
6
6
  } | {
7
7
  readonly kind: 'mint';
8
8
  readonly url: string;
9
+ }
10
+ /** The still is QUEUED. The tile shows loading and comes back, under a
11
+ * bound — `text` is what it says if the bound runs out. */
12
+ | {
13
+ readonly kind: 'deferred';
14
+ readonly reason: string;
15
+ readonly text: string;
16
+ readonly retryAfterMs: number | null;
9
17
  } | {
10
18
  readonly kind: 'unavailable';
11
19
  readonly reason: string;
@@ -14,14 +22,24 @@ export type ClipTileMedia = {
14
22
  readonly kind: 'none';
15
23
  readonly text: string;
16
24
  };
25
+ /** A 204, as its three headers arrived. `undefined` where a header was absent
26
+ * — which is a real case, not a defect: an addon older than D563 sends only
27
+ * the prose. */
28
+ export interface MintRefusalHeaders {
29
+ readonly code: string | undefined;
30
+ readonly reason: string;
31
+ readonly disposition: string | undefined;
32
+ readonly retryAfterMs?: number | null;
33
+ }
17
34
  /** What to draw for one clip's tile. */
18
35
  export declare function resolveTileMedia(clip: Clip): ClipTileMedia;
19
36
  /**
20
- * A mint that answered 204 — latch its `x-camstack-reason` to this instant.
37
+ * A 204, as something the tile can draw.
21
38
  *
22
- * The refusal is about NOW (a full queue, a sleeping camera), so it replaces
23
- * the offer for as long as the tile is mounted and is not retried behind the
24
- * operator's back. An unknown reason is shown raw rather than dropped: a
25
- * firmware inventing a refusal must not become a blank tile.
39
+ * `deferred` is the ONLY answer that comes back, and only because the server
40
+ * said so. Everything else — an unknown disposition, an absent one, a code
41
+ * this build has never heard of — is final, so the failure mode of a
42
+ * vocabulary that moves is a tile that stops early, never one that asks for
43
+ * ever.
26
44
  */
27
- export declare function latchMintRefusal(reason: string): ClipTileMedia;
45
+ export declare function describeRefusal(headers: MintRefusalHeaders): ClipTileMedia;
@@ -1,9 +1,27 @@
1
1
  import { Clip, ClipSource } from '@camstack/types';
2
+ import { ClipBand, ClipBandPlan } from './clip-band.js';
2
3
  import { ClipSourceNotice } from './clip-source-state.js';
3
4
  /** React Query's settled-ness, narrowed to what this decision needs. */
4
5
  export type ClipQueryStatus = 'pending' | 'error' | 'success';
5
6
  export interface ClipsViewInput {
6
- readonly dayId: string;
7
+ /**
8
+ * The period the list is about, and whether it may be asked for at all
9
+ * (`clip-band.ts`). A refused plan means NO query was fired, which is why
10
+ * the refusal has to be decided here rather than inferred from a query that
11
+ * will stay `pending` for ever.
12
+ */
13
+ readonly plan: ClipBandPlan;
14
+ /**
15
+ * The provider the clip list was asked of, or `null` when the camera lists
16
+ * NO source to ask.
17
+ *
18
+ * Naming one is required (D554 amended), so "nobody to ask" is a real state and it
19
+ * has to be told apart from "not answered yet": the query never fires, its
20
+ * status stays `pending` for ever, and a surface that only consults the
21
+ * status spins against a question with no addressee. Exactly the trap
22
+ * `too-wide` has, arriving from the other end.
23
+ */
24
+ readonly provider: string | null;
7
25
  readonly clipsStatus: ClipQueryStatus;
8
26
  readonly clips: readonly Clip[];
9
27
  readonly sourcesStatus: ClipQueryStatus;
@@ -16,9 +34,15 @@ export type ClipsViewState = {
16
34
  } | {
17
35
  readonly kind: 'failed';
18
36
  readonly message: string;
37
+ } | {
38
+ /** The band is wider than the bound, so nothing was asked. */
39
+ readonly kind: 'refused';
40
+ readonly band: ClipBand;
41
+ readonly localDays: number;
42
+ readonly maxLocalDays: number;
19
43
  } | {
20
44
  readonly kind: 'empty';
21
- readonly dayId: string;
45
+ readonly band: ClipBand;
22
46
  readonly answered: readonly ClipSourceNotice[];
23
47
  readonly silent: readonly ClipSourceNotice[];
24
48
  /** No source could answer at all — a different fact from a quiet day. */
@@ -1267,6 +1267,8 @@ export declare const usePipelineAnalyticsGetTrackMedia: typeof trpc.pipelineAnal
1267
1267
  export declare const usePipelineAnalyticsListTrackMedia: typeof trpc.pipelineAnalytics.listTrackMedia.useQuery;
1268
1268
  /** Generated alias around `trpc.pipelineAnalytics.listEventMedia.useQuery`. */
1269
1269
  export declare const usePipelineAnalyticsListEventMedia: typeof trpc.pipelineAnalytics.listEventMedia.useQuery;
1270
+ /** Generated alias around `trpc.pipelineAnalytics.ownersWithMedia.useQuery`. */
1271
+ export declare const usePipelineAnalyticsOwnersWithMedia: typeof trpc.pipelineAnalytics.ownersWithMedia.useQuery;
1270
1272
  /** Generated alias around `trpc.pipelineAnalytics.searchObjectEvents.useQuery`. */
1271
1273
  export declare const usePipelineAnalyticsSearchObjectEvents: typeof trpc.pipelineAnalytics.searchObjectEvents.useQuery;
1272
1274
  /** Generated alias around `trpc.pipelineAnalytics.wipeObjectEmbeddings.useMutation`. */