@camstack/ui-library 1.2.16 → 1.2.18
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.
- package/dist/composites/camera-stream-player.d.ts +16 -1
- package/dist/composites/cap-settings/RecordingSettings.d.ts +1 -5
- package/dist/composites/copy-button.d.ts +11 -2
- package/dist/composites/data-table-layout.d.ts +35 -0
- package/dist/composites/data-table.d.ts +9 -1
- package/dist/composites/device-list/columns.d.ts +27 -2
- package/dist/composites/index.d.ts +6 -0
- package/dist/composites/key-value-list.d.ts +8 -0
- package/dist/composites/reconnect-schedule.d.ts +32 -0
- package/dist/composites/setting-row.d.ts +24 -0
- package/dist/generated/system-hooks.d.ts +4 -0
- package/dist/hooks/turn-server-cache.d.ts +77 -0
- package/dist/index.cjs +407 -151
- package/dist/index.js +396 -152
- package/dist/lib/responsive.d.ts +28 -0
- package/dist/lib/tailwind-variants.d.ts +27 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3680,6 +3680,34 @@ var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
|
|
|
3680
3680
|
var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
|
|
3681
3681
|
/** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
|
|
3682
3682
|
var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
|
|
3683
|
+
/**
|
|
3684
|
+
* The tier boundary at which a settings label/value row stops stacking.
|
|
3685
|
+
*
|
|
3686
|
+
* Below it (L1 — compact) the row is ONE column: label on its own line, value
|
|
3687
|
+
* on the next at full width. Above it the row is label-left / value-right as
|
|
3688
|
+
* before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
|
|
3689
|
+
* value beside it; a phone has neither, and the failure mode is not "cramped"
|
|
3690
|
+
* but "the value is the part that gets cut" — the one thing the operator
|
|
3691
|
+
* opened the page to read.
|
|
3692
|
+
*/
|
|
3693
|
+
var SETTING_ROW_STACK_BREAKPOINT = "sm";
|
|
3694
|
+
/** Outer row: single column at L1, label/value columns from `sm` up. */
|
|
3695
|
+
var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
|
|
3696
|
+
/** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
|
|
3697
|
+
var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
|
|
3698
|
+
/**
|
|
3699
|
+
* Value lane — the value text plus its trailing affordances (copy / reveal).
|
|
3700
|
+
* `w-full` at L1 so the value owns the whole row once the label is above it;
|
|
3701
|
+
* `sm:flex-1` makes it share the row again from `sm` up.
|
|
3702
|
+
*/
|
|
3703
|
+
var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
|
|
3704
|
+
/**
|
|
3705
|
+
* The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
|
|
3706
|
+
* at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
|
|
3707
|
+
* desktop form, where the row is wide enough for truncation to be a choice
|
|
3708
|
+
* rather than data loss.
|
|
3709
|
+
*/
|
|
3710
|
+
var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
|
|
3683
3711
|
/** Section header label (uppercase tracking-wider). */
|
|
3684
3712
|
var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
|
|
3685
3713
|
/** Field label inside a row. */
|
|
@@ -13225,15 +13253,27 @@ var MOBILE_QUERY = "(max-width: 767px)";
|
|
|
13225
13253
|
* so sidebar fans out at the same point grids switch to multi-column.
|
|
13226
13254
|
*/
|
|
13227
13255
|
var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
|
|
13256
|
+
/**
|
|
13257
|
+
* `matchMedia` is universal in browsers but absent in bare DOM environments.
|
|
13258
|
+
* These hooks are called from shared primitives (`DataTable` picks its narrow
|
|
13259
|
+
* layout with one), so throwing here takes down the whole page that merely
|
|
13260
|
+
* rendered a table. Where the capability is missing, report the desktop
|
|
13261
|
+
* layout — a widescreen rendering is wrong-looking; a crashed page is gone.
|
|
13262
|
+
*/
|
|
13263
|
+
function matchQuery(query) {
|
|
13264
|
+
if (typeof window.matchMedia !== "function") return null;
|
|
13265
|
+
return window.matchMedia(query);
|
|
13266
|
+
}
|
|
13228
13267
|
function subscribeQuery(query) {
|
|
13229
13268
|
return (callback) => {
|
|
13230
|
-
const mql =
|
|
13269
|
+
const mql = matchQuery(query);
|
|
13270
|
+
if (mql === null) return () => {};
|
|
13231
13271
|
mql.addEventListener("change", callback);
|
|
13232
13272
|
return () => mql.removeEventListener("change", callback);
|
|
13233
13273
|
};
|
|
13234
13274
|
}
|
|
13235
13275
|
function getSnapshot(query) {
|
|
13236
|
-
return () =>
|
|
13276
|
+
return () => matchQuery(query)?.matches ?? false;
|
|
13237
13277
|
}
|
|
13238
13278
|
function getServerSnapshot() {
|
|
13239
13279
|
return false;
|
|
@@ -13651,18 +13691,75 @@ function Breadcrumb({ items, className }) {
|
|
|
13651
13691
|
});
|
|
13652
13692
|
}
|
|
13653
13693
|
//#endregion
|
|
13694
|
+
//#region src/composites/data-table-layout.ts
|
|
13695
|
+
/**
|
|
13696
|
+
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
13697
|
+
* Three columns still fit a phone at a readable size; four do not.
|
|
13698
|
+
*/
|
|
13699
|
+
var CARD_MODE_MIN_COLUMNS = 4;
|
|
13700
|
+
function resolveTableLayout({ mode, columnCount, isNarrow }) {
|
|
13701
|
+
if (!isNarrow) return "table";
|
|
13702
|
+
if (mode === "scroll") return "table";
|
|
13703
|
+
if (mode === "cards") return "cards";
|
|
13704
|
+
if (columnCount === 0) return "table";
|
|
13705
|
+
return columnCount >= 4 ? "cards" : "table";
|
|
13706
|
+
}
|
|
13707
|
+
//#endregion
|
|
13708
|
+
//#region src/composites/setting-row.tsx
|
|
13709
|
+
function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
|
|
13710
|
+
const LabelTag = elements === "description" ? "dt" : "span";
|
|
13711
|
+
const ValueTag = elements === "description" ? "dd" : "div";
|
|
13712
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
13713
|
+
"data-setting-row": "",
|
|
13714
|
+
className: cn(SETTING_ROW, className),
|
|
13715
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelTag, {
|
|
13716
|
+
"data-setting-row-label": "",
|
|
13717
|
+
className: SETTING_ROW_LABEL,
|
|
13718
|
+
children: label
|
|
13719
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ValueTag, {
|
|
13720
|
+
"data-setting-row-value": "",
|
|
13721
|
+
className: SETTING_ROW_VALUE,
|
|
13722
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
13723
|
+
"data-setting-row-value-text": "",
|
|
13724
|
+
className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
|
|
13725
|
+
children
|
|
13726
|
+
}), actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
13727
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
13728
|
+
children: actions
|
|
13729
|
+
})]
|
|
13730
|
+
})]
|
|
13731
|
+
});
|
|
13732
|
+
}
|
|
13733
|
+
//#endregion
|
|
13654
13734
|
//#region src/composites/data-table.tsx
|
|
13655
13735
|
var ALIGN_CLASS = {
|
|
13656
13736
|
left: "text-left",
|
|
13657
13737
|
right: "text-right",
|
|
13658
13738
|
center: "text-center"
|
|
13659
13739
|
};
|
|
13660
|
-
function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName }) {
|
|
13740
|
+
function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, mobileMode = "auto" }) {
|
|
13741
|
+
const isNarrow = useIsMobile();
|
|
13742
|
+
const layout = resolveTableLayout({
|
|
13743
|
+
mode: mobileMode,
|
|
13744
|
+
columnCount: columns.length,
|
|
13745
|
+
isNarrow
|
|
13746
|
+
});
|
|
13661
13747
|
if (rows.length === 0 && emptyMessage) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
13662
13748
|
className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface px-3 py-4 text-xs text-foreground-subtle text-center ${className ?? ""}`,
|
|
13663
13749
|
children: emptyMessage
|
|
13664
13750
|
});
|
|
13665
13751
|
if (rows.length === 0) return null;
|
|
13752
|
+
if (layout === "cards") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
13753
|
+
className: `space-y-2 ${className ?? ""}`,
|
|
13754
|
+
children: rows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTableCard, {
|
|
13755
|
+
row,
|
|
13756
|
+
rowIndex,
|
|
13757
|
+
columns,
|
|
13758
|
+
bordered,
|
|
13759
|
+
onRowClick,
|
|
13760
|
+
rowClassName
|
|
13761
|
+
}, rowKey ? rowKey(row, rowIndex) : rowIndex))
|
|
13762
|
+
});
|
|
13666
13763
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
13667
13764
|
className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface overflow-x-auto ${className ?? ""}`,
|
|
13668
13765
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
@@ -13707,6 +13804,36 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
|
|
|
13707
13804
|
})
|
|
13708
13805
|
});
|
|
13709
13806
|
}
|
|
13807
|
+
/**
|
|
13808
|
+
* One row as a card. Columns that carry a plain-text `header` become labelled
|
|
13809
|
+
* `SettingRow`s — the label is the column title and the value gets the full
|
|
13810
|
+
* card width. Columns with no text header (an actions column, a
|
|
13811
|
+
* `headerRender`-only column) have no label to show, so they render as a
|
|
13812
|
+
* full-width strip at the foot of the card.
|
|
13813
|
+
*/
|
|
13814
|
+
function DataTableCard({ row, rowIndex, columns, bordered, onRowClick, rowClassName }) {
|
|
13815
|
+
const labelled = columns.filter((col) => col.header !== void 0 && col.header !== "");
|
|
13816
|
+
const unlabelled = columns.filter((col) => col.header === void 0 || col.header === "");
|
|
13817
|
+
const interactive = onRowClick !== void 0;
|
|
13818
|
+
const extra = rowClassName?.(row, rowIndex) ?? "";
|
|
13819
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
13820
|
+
"data-data-table-card": "",
|
|
13821
|
+
onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
|
|
13822
|
+
className: [
|
|
13823
|
+
"rounded-lg bg-surface px-3 py-1.5 divide-y divide-border-subtle",
|
|
13824
|
+
bordered ? "border border-border" : "",
|
|
13825
|
+
interactive ? "cursor-pointer hover:bg-primary/5" : "",
|
|
13826
|
+
extra
|
|
13827
|
+
].filter(Boolean).join(" "),
|
|
13828
|
+
children: [labelled.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
|
|
13829
|
+
label: col.header,
|
|
13830
|
+
children: col.render(row, rowIndex)
|
|
13831
|
+
}, col.key)), unlabelled.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
13832
|
+
className: "flex flex-wrap items-center justify-end gap-2 py-1.5",
|
|
13833
|
+
children: unlabelled.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: col.render(row, rowIndex) }, col.key))
|
|
13834
|
+
})]
|
|
13835
|
+
});
|
|
13836
|
+
}
|
|
13710
13837
|
//#endregion
|
|
13711
13838
|
//#region src/composites/slide-over-panel.tsx
|
|
13712
13839
|
/**
|
|
@@ -15839,18 +15966,21 @@ function StatCard({ value, label, trend, className }) {
|
|
|
15839
15966
|
}
|
|
15840
15967
|
//#endregion
|
|
15841
15968
|
//#region src/composites/key-value-list.tsx
|
|
15969
|
+
/**
|
|
15970
|
+
* A `<dl>` of label/value rows. Layout (including the L1 stacking) is owned by
|
|
15971
|
+
* `SettingRow`; this composite only supplies the description-list semantics.
|
|
15972
|
+
*
|
|
15973
|
+
* The rows used to be `flex items-center h-7` with a `w-1/3` term, so a label
|
|
15974
|
+
* could neither wrap nor stack: on a phone it took a third of the width and
|
|
15975
|
+
* the value took whatever was left.
|
|
15976
|
+
*/
|
|
15842
15977
|
function KeyValueList({ items, className }) {
|
|
15843
15978
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
|
|
15844
15979
|
className: cn("flex flex-col", className),
|
|
15845
|
-
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.
|
|
15846
|
-
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
children: item.key
|
|
15850
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
15851
|
-
className: "text-foreground text-xs",
|
|
15852
|
-
children: item.value
|
|
15853
|
-
})]
|
|
15980
|
+
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
|
|
15981
|
+
elements: "description",
|
|
15982
|
+
label: item.key,
|
|
15983
|
+
children: item.value
|
|
15854
15984
|
}, item.key))
|
|
15855
15985
|
});
|
|
15856
15986
|
}
|
|
@@ -16218,7 +16348,8 @@ function DeviceGrid({ children, minCardWidth = 220, gap = 3, className }) {
|
|
|
16218
16348
|
* longest). So as the viewport shrinks the HIGHEST-priority-number column
|
|
16219
16349
|
* drops first.
|
|
16220
16350
|
*
|
|
16221
|
-
* name (0) — never hidden;
|
|
16351
|
+
* name (0) — never hidden; pinned left from `md` up (see
|
|
16352
|
+
* `NAME_COLUMN_PIN_CLASS`), scrolls below it
|
|
16222
16353
|
* previewActions (1) — never hidden; carries the live control + status dot
|
|
16223
16354
|
* icon (2) — integration badge; rendered INSIDE the name cell,
|
|
16224
16355
|
* not a standalone column, so it has no breakpoint
|
|
@@ -16270,7 +16401,7 @@ function columnsForContext(ctx) {
|
|
|
16270
16401
|
}
|
|
16271
16402
|
/**
|
|
16272
16403
|
* Lower number = higher priority (kept longest as width shrinks). `name` = 0
|
|
16273
|
-
* (
|
|
16404
|
+
* (never dropped); `previewActions` = 1 (always visible). The optional
|
|
16274
16405
|
* columns drop in REVERSE priority order as width shrinks — highest number
|
|
16275
16406
|
* (`type`) hides first, then `features`. `icon` is a name-cell badge, not a
|
|
16276
16407
|
* standalone column. See `COLUMN_BREAKPOINT_CLASS` for the derived classes.
|
|
@@ -16306,6 +16437,14 @@ var COLUMN_BREAKPOINT_CLASS = {
|
|
|
16306
16437
|
type: "hidden lg:table-cell",
|
|
16307
16438
|
manufacturer: "hidden xl:table-cell"
|
|
16308
16439
|
};
|
|
16440
|
+
/** Pin classes for the NAME `<th>`/`<td>` — never unconditional. */
|
|
16441
|
+
var NAME_COLUMN_PIN_CLASS = "md:sticky md:left-0 md:z-[1]";
|
|
16442
|
+
/**
|
|
16443
|
+
* NAME column width. Narrower below the pin breakpoint so NAME + Preview fit a
|
|
16444
|
+
* phone viewport without a horizontal scroll at all; the roomier desktop width
|
|
16445
|
+
* returns with the pin.
|
|
16446
|
+
*/
|
|
16447
|
+
var NAME_COLUMN_WIDTH_CLASS = "w-44 max-w-[11rem] md:w-64 md:max-w-[16rem]";
|
|
16309
16448
|
//#endregion
|
|
16310
16449
|
//#region src/composites/device-list/hardware.ts
|
|
16311
16450
|
var MANUFACTURER_KEY = "manufacturer";
|
|
@@ -19382,6 +19521,10 @@ var useSettingsStoreInsert = trpc.settingsStore.insert.useMutation;
|
|
|
19382
19521
|
var useSettingsStoreUpdate = trpc.settingsStore.update.useMutation;
|
|
19383
19522
|
/** Generated alias around `trpc.settingsStore.delete.useMutation`. */
|
|
19384
19523
|
var useSettingsStoreDelete = trpc.settingsStore.delete.useMutation;
|
|
19524
|
+
/** Generated alias around `trpc.settingsStore.deleteWhere.useMutation`. */
|
|
19525
|
+
var useSettingsStoreDeleteWhere = trpc.settingsStore.deleteWhere.useMutation;
|
|
19526
|
+
/** Generated alias around `trpc.settingsStore.updateWhere.useMutation`. */
|
|
19527
|
+
var useSettingsStoreUpdateWhere = trpc.settingsStore.updateWhere.useMutation;
|
|
19385
19528
|
/** Generated alias around `trpc.settingsStore.count.useQuery`. */
|
|
19386
19529
|
var useSettingsStoreCount = trpc.settingsStore.count.useQuery;
|
|
19387
19530
|
/** Generated alias around `trpc.settingsStore.histogram.useQuery`. */
|
|
@@ -29433,7 +29576,7 @@ function DeviceItemTableRow(props) {
|
|
|
29433
29576
|
className: cn("group cursor-pointer hover:bg-surface-hover transition-colors", !hasChildren && "border-b border-border-subtle/40", isAccessoryRow && "bg-foreground-subtle/[0.03]", rowSelected && "bg-primary/10", className),
|
|
29434
29577
|
children: [
|
|
29435
29578
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
29436
|
-
className: cn("
|
|
29579
|
+
className: cn(NAME_COLUMN_PIN_CLASS, "py-1.5 align-middle pr-2 transition-colors", rowSelected ? "bg-primary/10 group-hover:bg-surface-hover" : isAccessoryRow ? "bg-surface-subtle group-hover:bg-surface-hover" : "bg-surface group-hover:bg-surface-hover", NAME_COLUMN_WIDTH_CLASS, "min-w-0 overflow-hidden", INDENT_CLASS[indentLevel]),
|
|
29437
29580
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
29438
29581
|
className: "flex items-center gap-1.5",
|
|
29439
29582
|
children: [selection && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -30436,7 +30579,7 @@ function TableLayout({ rows, accessoriesByParent, autoExpandedParents, devices,
|
|
|
30436
30579
|
children: [
|
|
30437
30580
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
30438
30581
|
"aria-sort": ariaSortFor(sort, "name"),
|
|
30439
|
-
className:
|
|
30582
|
+
className: cn(NAME_COLUMN_PIN_CLASS, NAME_COLUMN_WIDTH_CLASS, "bg-surface text-left px-2 py-2 text-[9.5px] font-medium uppercase tracking-wider text-foreground-subtle"),
|
|
30440
30583
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SortableHeaderButton, {
|
|
30441
30584
|
columnId: "name",
|
|
30442
30585
|
label: "Name",
|
|
@@ -35086,15 +35229,14 @@ var MODES = [
|
|
|
35086
35229
|
label: "Continuous"
|
|
35087
35230
|
}
|
|
35088
35231
|
];
|
|
35089
|
-
function RecordingSettings({ initial, saving, onSave
|
|
35232
|
+
function RecordingSettings({ initial, saving, onSave }) {
|
|
35090
35233
|
const [tab, setTab] = (0, react$1.useState)(isAdvancedConfig(initial) ? "advanced" : "base");
|
|
35091
35234
|
const [base, setBase] = (0, react$1.useState)(formStateFromConfig(initial));
|
|
35092
35235
|
const [bands, setBands] = (0, react$1.useState)([...initial.bands]);
|
|
35093
35236
|
const [common, setCommon] = (0, react$1.useState)({
|
|
35094
35237
|
profiles: initial.profiles,
|
|
35095
35238
|
segmentSeconds: initial.segmentSeconds,
|
|
35096
|
-
retention: initial.retention
|
|
35097
|
-
stripsEnabled: initial.stripsEnabled
|
|
35239
|
+
retention: initial.retention
|
|
35098
35240
|
});
|
|
35099
35241
|
const ret = common.retention ?? {};
|
|
35100
35242
|
const setRet = (patch) => setCommon({
|
|
@@ -35132,8 +35274,7 @@ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regene
|
|
|
35132
35274
|
const shared = {
|
|
35133
35275
|
profiles: common.profiles ? [...common.profiles] : void 0,
|
|
35134
35276
|
segmentSeconds: common.segmentSeconds,
|
|
35135
|
-
retention: common.retention
|
|
35136
|
-
...common.stripsEnabled === void 0 ? {} : { stripsEnabled: common.stripsEnabled }
|
|
35277
|
+
retention: common.retention
|
|
35137
35278
|
};
|
|
35138
35279
|
if (tab === "base") onSave({
|
|
35139
35280
|
...configFromFormState({
|
|
@@ -35285,65 +35426,37 @@ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regene
|
|
|
35285
35426
|
}),
|
|
35286
35427
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35287
35428
|
className: "flex flex-wrap items-center gap-4 text-xs",
|
|
35288
|
-
children: [
|
|
35289
|
-
|
|
35290
|
-
|
|
35291
|
-
|
|
35292
|
-
|
|
35293
|
-
|
|
35294
|
-
|
|
35295
|
-
|
|
35296
|
-
|
|
35297
|
-
|
|
35298
|
-
|
|
35299
|
-
|
|
35300
|
-
|
|
35301
|
-
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
"
|
|
35307
|
-
|
|
35308
|
-
|
|
35309
|
-
|
|
35310
|
-
value: common.segmentSeconds ?? "",
|
|
35311
|
-
placeholder: "default",
|
|
35312
|
-
onChange: (e) => setCommon({
|
|
35313
|
-
...common,
|
|
35314
|
-
segmentSeconds: numOrUndef(e.target.value)
|
|
35315
|
-
}),
|
|
35316
|
-
className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
|
|
35317
|
-
}),
|
|
35318
|
-
"s"
|
|
35319
|
-
]
|
|
35320
|
-
}),
|
|
35321
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
35322
|
-
className: "flex items-center gap-1",
|
|
35323
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
35324
|
-
type: "checkbox",
|
|
35325
|
-
checked: common.stripsEnabled === true,
|
|
35429
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35430
|
+
className: "flex items-center gap-2",
|
|
35431
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35432
|
+
className: "text-foreground-subtle",
|
|
35433
|
+
children: "Profiles:"
|
|
35434
|
+
}), PROFILES.map((p) => {
|
|
35435
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35436
|
+
type: "button",
|
|
35437
|
+
onClick: () => toggleProfile(p),
|
|
35438
|
+
className: `rounded px-2 py-1 ${common.profiles == null || common.profiles.includes(p) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
|
|
35439
|
+
children: p
|
|
35440
|
+
}, p);
|
|
35441
|
+
})]
|
|
35442
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
35443
|
+
className: "flex items-center gap-1",
|
|
35444
|
+
children: [
|
|
35445
|
+
"Segment",
|
|
35446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
35447
|
+
type: "number",
|
|
35448
|
+
min: 1,
|
|
35449
|
+
value: common.segmentSeconds ?? "",
|
|
35450
|
+
placeholder: "default",
|
|
35326
35451
|
onChange: (e) => setCommon({
|
|
35327
35452
|
...common,
|
|
35328
|
-
|
|
35329
|
-
})
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
|
|
35333
|
-
|
|
35334
|
-
|
|
35335
|
-
onRegenerateStrips ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35336
|
-
type: "button",
|
|
35337
|
-
onClick: onRegenerateStrips,
|
|
35338
|
-
disabled: regeneratingStrips === true,
|
|
35339
|
-
className: "rounded-md border border-border bg-surface-hover px-2 py-1 text-xs text-foreground-subtle transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
35340
|
-
children: regeneratingStrips === true ? "Regenerating…" : "Regenerate strips"
|
|
35341
|
-
}) : null
|
|
35342
|
-
]
|
|
35343
|
-
}),
|
|
35344
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
35345
|
-
className: "text-[11px] text-foreground-subtle",
|
|
35346
|
-
children: "OPT-IN: save every keyframe of the low recording as a JPEG strip for fluid fast scrubbing. Costs disk (a derived cache, reclaimed with the footage). Regenerate clears and rebuilds the recent days from the segments already on disk."
|
|
35453
|
+
segmentSeconds: numOrUndef(e.target.value)
|
|
35454
|
+
}),
|
|
35455
|
+
className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
|
|
35456
|
+
}),
|
|
35457
|
+
"s"
|
|
35458
|
+
]
|
|
35459
|
+
})]
|
|
35347
35460
|
}),
|
|
35348
35461
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35349
35462
|
className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
@@ -36592,14 +36705,6 @@ function RecordingPanel({ deviceId }) {
|
|
|
36592
36705
|
const configQuery = useRecordingGetDeviceConfig({ deviceId });
|
|
36593
36706
|
const setConfig = useRecordingSetDeviceConfig();
|
|
36594
36707
|
const rescanStorage = useRecordingRescanStorage();
|
|
36595
|
-
const customAction = useAddonsCustom();
|
|
36596
|
-
const regenerateStrips = (0, react$1.useCallback)(() => {
|
|
36597
|
-
customAction.mutate({
|
|
36598
|
-
addonId: "recorder",
|
|
36599
|
-
action: "regenerateStrips",
|
|
36600
|
-
input: { deviceId }
|
|
36601
|
-
});
|
|
36602
|
-
}, [customAction, deviceId]);
|
|
36603
36708
|
const saveConfig = (config) => {
|
|
36604
36709
|
setConfig.mutate({
|
|
36605
36710
|
deviceId,
|
|
@@ -36641,9 +36746,7 @@ function RecordingPanel({ deviceId }) {
|
|
|
36641
36746
|
children: resolvedConfig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingSettings, {
|
|
36642
36747
|
initial: resolvedConfig,
|
|
36643
36748
|
saving: setConfig.isPending,
|
|
36644
|
-
onSave: saveConfig
|
|
36645
|
-
onRegenerateStrips: regenerateStrips,
|
|
36646
|
-
regeneratingStrips: customAction.isPending
|
|
36749
|
+
onSave: saveConfig
|
|
36647
36750
|
}, JSON.stringify(resolvedConfig)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36648
36751
|
className: "px-1 py-2 text-xs text-foreground-subtle",
|
|
36649
36752
|
children: "Loading settings…"
|
|
@@ -38979,9 +39082,9 @@ function ObjectArrayField({ field }) {
|
|
|
38979
39082
|
className: "rounded-md border border-border bg-surface-subtle px-3 py-2 text-xs text-foreground-subtle",
|
|
38980
39083
|
children: field.emptyMessage ?? "No entries"
|
|
38981
39084
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
38982
|
-
className: "rounded-md border border-border overflow-
|
|
39085
|
+
className: "rounded-md border border-border overflow-x-auto",
|
|
38983
39086
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
38984
|
-
className: "w-full text-xs",
|
|
39087
|
+
className: "w-full min-w-[28rem] text-xs",
|
|
38985
39088
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
38986
39089
|
className: "bg-surface-subtle border-b border-border",
|
|
38987
39090
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: field.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
@@ -40208,6 +40311,31 @@ function DetectionBoxes({ detections, frameWidth, frameHeight }) {
|
|
|
40208
40311
|
}) });
|
|
40209
40312
|
}
|
|
40210
40313
|
//#endregion
|
|
40314
|
+
//#region src/composites/reconnect-schedule.ts
|
|
40315
|
+
/** Exponential backoff, ±20% jitter, bounded attempts. The bound exists so a
|
|
40316
|
+
* permanently-dead stream stops consuming signaling; the EXHAUSTED action is
|
|
40317
|
+
* the caller's cue to surface a hard error (never to go silent). */
|
|
40318
|
+
var RECONNECT_POLICY = {
|
|
40319
|
+
baseDelayMs: 1500,
|
|
40320
|
+
maxDelayMs: 15e3,
|
|
40321
|
+
maxAttempts: 40
|
|
40322
|
+
};
|
|
40323
|
+
/**
|
|
40324
|
+
* Decide what attempt number `attempt` (0-based) should do. `random` is the
|
|
40325
|
+
* jitter source (unit interval), injectable for tests.
|
|
40326
|
+
*/
|
|
40327
|
+
function nextReconnectAction(attempt, random = Math.random) {
|
|
40328
|
+
if (attempt >= RECONNECT_POLICY.maxAttempts) return {
|
|
40329
|
+
kind: "exhausted",
|
|
40330
|
+
attempts: attempt
|
|
40331
|
+
};
|
|
40332
|
+
const base = Math.min(RECONNECT_POLICY.maxDelayMs, RECONNECT_POLICY.baseDelayMs * 2 ** attempt);
|
|
40333
|
+
return {
|
|
40334
|
+
kind: "retry",
|
|
40335
|
+
delayMs: Math.round(base * (.8 + random() * .4))
|
|
40336
|
+
};
|
|
40337
|
+
}
|
|
40338
|
+
//#endregion
|
|
40211
40339
|
//#region src/composites/camera-stream-player.tsx
|
|
40212
40340
|
/**
|
|
40213
40341
|
* Silence (or restore) a live WebRTC stream FOR REAL.
|
|
@@ -40270,11 +40398,8 @@ function computeClientHints(container) {
|
|
|
40270
40398
|
}
|
|
40271
40399
|
return hints;
|
|
40272
40400
|
}
|
|
40273
|
-
var RECONNECT_BASE_DELAY_MS = 1500;
|
|
40274
|
-
var RECONNECT_MAX_DELAY_MS = 15e3;
|
|
40275
|
-
var MAX_RECONNECT_ATTEMPTS = 40;
|
|
40276
40401
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
40277
|
-
function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
|
|
40402
|
+
function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement }) {
|
|
40278
40403
|
const videoRef = (0, react$1.useRef)(null);
|
|
40279
40404
|
const containerRef = (0, react$1.useRef)(null);
|
|
40280
40405
|
const pcRef = (0, react$1.useRef)(null);
|
|
@@ -40290,6 +40415,14 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40290
40415
|
* consumer, without re-creating the connect callback on every render. */
|
|
40291
40416
|
const onControlChannelRef = (0, react$1.useRef)(onControlChannel);
|
|
40292
40417
|
onControlChannelRef.current = onControlChannel;
|
|
40418
|
+
/** Same pattern for the video-element handle: delivered once on mount, null
|
|
40419
|
+
* on unmount — the ref keeps the latest consumer without re-running. */
|
|
40420
|
+
const onVideoElementRef = (0, react$1.useRef)(onVideoElement);
|
|
40421
|
+
onVideoElementRef.current = onVideoElement;
|
|
40422
|
+
(0, react$1.useEffect)(() => {
|
|
40423
|
+
onVideoElementRef.current?.(videoRef.current);
|
|
40424
|
+
return () => onVideoElementRef.current?.(null);
|
|
40425
|
+
}, []);
|
|
40293
40426
|
/** The live session being polled for `pendingRenegotiation` (client-offer). */
|
|
40294
40427
|
const activeSessionIdRef = (0, react$1.useRef)(null);
|
|
40295
40428
|
/** Timer for the session-state (renegotiation) poll loop. */
|
|
@@ -40678,6 +40811,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40678
40811
|
iceConnectedMs: timing.iceConnectedMs,
|
|
40679
40812
|
firstTrackMs: timing.firstTrackMs
|
|
40680
40813
|
});
|
|
40814
|
+
reportHardErrorRef.current("no decoded frame after connect");
|
|
40815
|
+
scheduleReconnect();
|
|
40681
40816
|
}, FIRST_FRAME_TIMEOUT_MS);
|
|
40682
40817
|
}
|
|
40683
40818
|
}
|
|
@@ -40946,17 +41081,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40946
41081
|
]);
|
|
40947
41082
|
const connect = useClientOffer ? connectClientOffer : useServerOffer ? connectServerOffer : connectWhep;
|
|
40948
41083
|
const connectRef = (0, react$1.useRef)(() => {});
|
|
41084
|
+
const onReconnectAttemptRef = (0, react$1.useRef)(onReconnectAttempt);
|
|
41085
|
+
onReconnectAttemptRef.current = onReconnectAttempt;
|
|
41086
|
+
const reportHardErrorRef = (0, react$1.useRef)(() => {});
|
|
41087
|
+
reportHardErrorRef.current = (msg) => {
|
|
41088
|
+
console.warn("[WebRTC] hard error", {
|
|
41089
|
+
streamKey,
|
|
41090
|
+
msg
|
|
41091
|
+
});
|
|
41092
|
+
setErrorMessage(msg);
|
|
41093
|
+
onError?.(msg);
|
|
41094
|
+
updateState("error");
|
|
41095
|
+
};
|
|
40949
41096
|
connectRef.current = connect;
|
|
40950
41097
|
const scheduleReconnect = (0, react$1.useCallback)(() => {
|
|
40951
41098
|
if (!mountedRef.current) return;
|
|
40952
|
-
if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) return;
|
|
40953
41099
|
const attempt = reconnectAttemptsRef.current;
|
|
41100
|
+
const action = nextReconnectAction(attempt);
|
|
41101
|
+
onReconnectAttemptRef.current?.(action);
|
|
41102
|
+
if (action.kind === "exhausted") {
|
|
41103
|
+
reportHardErrorRef.current(`reconnect attempts exhausted (${action.attempts})`);
|
|
41104
|
+
return;
|
|
41105
|
+
}
|
|
40954
41106
|
reconnectAttemptsRef.current += 1;
|
|
40955
|
-
const base = Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** attempt);
|
|
40956
|
-
const delay = Math.round(base * (.8 + Math.random() * .4));
|
|
40957
41107
|
reconnectTimerRef.current = setTimeout(() => {
|
|
40958
41108
|
if (mountedRef.current) connectRef.current();
|
|
40959
|
-
},
|
|
41109
|
+
}, action.delayMs);
|
|
40960
41110
|
}, []);
|
|
40961
41111
|
(0, react$1.useEffect)(() => {
|
|
40962
41112
|
mountedRef.current = true;
|
|
@@ -41337,7 +41487,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
41337
41487
|
children: statsText
|
|
41338
41488
|
}),
|
|
41339
41489
|
overlay,
|
|
41340
|
-
!stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41490
|
+
showControls && !stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41341
41491
|
className: "absolute inset-0 z-10 transform-gpu flex flex-col items-center justify-center bg-black/70 gap-2",
|
|
41342
41492
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
41343
41493
|
className: "h-6 w-6 text-white/60 animate-spin",
|
|
@@ -41359,7 +41509,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
41359
41509
|
children: "Connecting…"
|
|
41360
41510
|
})]
|
|
41361
41511
|
}),
|
|
41362
|
-
!stillImg && (state === "error" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41512
|
+
showControls && !stillImg && (state === "error" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41363
41513
|
className: "absolute inset-0 z-10 transform-gpu flex flex-col items-center justify-center bg-black/70 gap-2",
|
|
41364
41514
|
children: [
|
|
41365
41515
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -41384,7 +41534,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
41384
41534
|
})
|
|
41385
41535
|
]
|
|
41386
41536
|
}),
|
|
41387
|
-
stillImg && (state === "connecting" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
41537
|
+
showControls && stillImg && (state === "connecting" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
41388
41538
|
className: "absolute top-2 left-2 z-10 transform-gpu flex h-6 w-6 items-center justify-center rounded-full bg-black/55",
|
|
41389
41539
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
41390
41540
|
className: "h-3.5 w-3.5 text-white/90 animate-spin",
|
|
@@ -41403,7 +41553,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
41403
41553
|
})
|
|
41404
41554
|
})
|
|
41405
41555
|
}),
|
|
41406
|
-
stillImg && state === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
41556
|
+
showControls && stillImg && state === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
41407
41557
|
onClick: handleReconnect,
|
|
41408
41558
|
title: errorMessage || "Reconnect",
|
|
41409
41559
|
className: "absolute top-2 left-2 z-10 transform-gpu flex h-6 w-6 items-center justify-center rounded-full bg-black/55 text-white/90 hover:bg-black/75 transition-colors",
|
|
@@ -42517,7 +42667,7 @@ function StreamBrokerSelector({ deviceId, value, onChange, disabled, label, clas
|
|
|
42517
42667
|
* needs a one-click copy affordance (export setup panels, etc.).
|
|
42518
42668
|
*/
|
|
42519
42669
|
var COPIED_RESET_MS = 2e3;
|
|
42520
|
-
function CopyButton({ value, label, className, disabled }) {
|
|
42670
|
+
function CopyButton({ value, label, srLabel, className, disabled }) {
|
|
42521
42671
|
const [copied, setCopied] = (0, react$1.useState)(false);
|
|
42522
42672
|
const handleCopy = (0, react$1.useCallback)(() => {
|
|
42523
42673
|
if (!value) return;
|
|
@@ -42533,7 +42683,7 @@ function CopyButton({ value, label, className, disabled }) {
|
|
|
42533
42683
|
disabled: disabled || value.length === 0,
|
|
42534
42684
|
onClick: handleCopy,
|
|
42535
42685
|
className: cn(className),
|
|
42536
|
-
"aria-label": copied ? "Copied" : `Copy ${label ?? "value"}`,
|
|
42686
|
+
"aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
|
|
42537
42687
|
children: [copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" }), label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
42538
42688
|
className: "ml-1",
|
|
42539
42689
|
children: copied ? "Copied" : label
|
|
@@ -42602,35 +42752,30 @@ function capitaliseLinkState(state) {
|
|
|
42602
42752
|
/**
|
|
42603
42753
|
* A single label/value row in the Setup section. `secret` rows mask the
|
|
42604
42754
|
* value behind a reveal toggle; every row gets a copy button.
|
|
42755
|
+
*
|
|
42756
|
+
* Layout — including the L1 stack that keeps a long token readable on a phone
|
|
42757
|
+
* — is owned by the shared `<SettingRow>`; this component only decides what
|
|
42758
|
+
* the value and the affordances are.
|
|
42605
42759
|
*/
|
|
42606
42760
|
function SetupFieldRow({ field }) {
|
|
42607
42761
|
const [revealed, setRevealed] = (0, react$1.useState)(false);
|
|
42608
42762
|
const isSecret = field.secret === true;
|
|
42609
42763
|
const displayValue = isSecret && !revealed ? "•".repeat(Math.min(field.value.length, 24)) : field.value;
|
|
42610
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
42611
|
-
|
|
42612
|
-
|
|
42613
|
-
|
|
42614
|
-
|
|
42615
|
-
|
|
42616
|
-
|
|
42617
|
-
|
|
42618
|
-
|
|
42619
|
-
|
|
42620
|
-
|
|
42621
|
-
|
|
42622
|
-
|
|
42623
|
-
|
|
42624
|
-
|
|
42625
|
-
"aria-label": revealed ? "Hide value" : "Reveal value",
|
|
42626
|
-
onClick: () => setRevealed((v) => !v),
|
|
42627
|
-
children: revealed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
|
|
42628
|
-
}),
|
|
42629
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
42630
|
-
value: field.value,
|
|
42631
|
-
label: field.label
|
|
42632
|
-
})
|
|
42633
|
-
]
|
|
42764
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
|
|
42765
|
+
label: field.label,
|
|
42766
|
+
valueClassName: "font-mono",
|
|
42767
|
+
actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isSecret && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
42768
|
+
size: "sm",
|
|
42769
|
+
variant: "ghost",
|
|
42770
|
+
type: "button",
|
|
42771
|
+
"aria-label": revealed ? "Hide value" : "Reveal value",
|
|
42772
|
+
onClick: () => setRevealed((v) => !v),
|
|
42773
|
+
children: revealed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
|
|
42774
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
42775
|
+
value: field.value,
|
|
42776
|
+
srLabel: field.label
|
|
42777
|
+
})] }),
|
|
42778
|
+
children: displayValue || "—"
|
|
42634
42779
|
});
|
|
42635
42780
|
}
|
|
42636
42781
|
/**
|
|
@@ -46736,6 +46881,112 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
46736
46881
|
};
|
|
46737
46882
|
}
|
|
46738
46883
|
//#endregion
|
|
46884
|
+
//#region src/hooks/turn-server-cache.ts
|
|
46885
|
+
/** Short enough that rotated credentials are picked up promptly. */
|
|
46886
|
+
var DEFAULT_FRESH_TTL_MS = 5 * 6e4;
|
|
46887
|
+
/** Half of the shortest provider credential lifetime in the fleet (24 h). */
|
|
46888
|
+
var DEFAULT_STALE_CAP_MS = 720 * 6e4;
|
|
46889
|
+
var DEFAULT_STORAGE_KEY = "camstack:turn-servers:v1";
|
|
46890
|
+
function defaultStorage() {
|
|
46891
|
+
const g = globalThis;
|
|
46892
|
+
try {
|
|
46893
|
+
return g.localStorage ?? null;
|
|
46894
|
+
} catch {
|
|
46895
|
+
return null;
|
|
46896
|
+
}
|
|
46897
|
+
}
|
|
46898
|
+
/** Type guard for a persisted entry — storage content is external data. */
|
|
46899
|
+
function isCacheEntry(value) {
|
|
46900
|
+
if (typeof value !== "object" || value === null) return false;
|
|
46901
|
+
const v = value;
|
|
46902
|
+
if (typeof v.fetchedAt !== "number" || !Array.isArray(v.servers)) return false;
|
|
46903
|
+
return v.servers.every((s) => {
|
|
46904
|
+
if (typeof s !== "object" || s === null) return false;
|
|
46905
|
+
const srv = s;
|
|
46906
|
+
if (!(typeof srv.urls === "string" || Array.isArray(srv.urls) && srv.urls.every((u) => typeof u === "string"))) return false;
|
|
46907
|
+
if (srv.username !== void 0 && typeof srv.username !== "string") return false;
|
|
46908
|
+
if (srv.credential !== void 0 && typeof srv.credential !== "string") return false;
|
|
46909
|
+
return true;
|
|
46910
|
+
});
|
|
46911
|
+
}
|
|
46912
|
+
var TurnServerCache = class {
|
|
46913
|
+
freshTtlMs;
|
|
46914
|
+
staleCapMs;
|
|
46915
|
+
storage;
|
|
46916
|
+
now;
|
|
46917
|
+
storageKey;
|
|
46918
|
+
/** Keyed on the caller's tRPC client object (stable per connected system). */
|
|
46919
|
+
memory = /* @__PURE__ */ new WeakMap();
|
|
46920
|
+
/** In-flight fetch per key — concurrent callers share one round trip. */
|
|
46921
|
+
inflight = /* @__PURE__ */ new WeakMap();
|
|
46922
|
+
constructor(options = {}) {
|
|
46923
|
+
this.freshTtlMs = options.freshTtlMs ?? DEFAULT_FRESH_TTL_MS;
|
|
46924
|
+
this.staleCapMs = options.staleCapMs ?? DEFAULT_STALE_CAP_MS;
|
|
46925
|
+
this.storage = "storage" in options ? options.storage ?? null : defaultStorage();
|
|
46926
|
+
this.now = options.now ?? (() => Date.now());
|
|
46927
|
+
this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
46928
|
+
}
|
|
46929
|
+
/**
|
|
46930
|
+
* Resolve the ICE servers for `key`, fetching via `fetch` only when no
|
|
46931
|
+
* fresh-enough entry exists. Never rejects: a failed fetch resolves to the
|
|
46932
|
+
* stale entry when one exists, else `undefined`.
|
|
46933
|
+
*/
|
|
46934
|
+
async getOrFetch(key, fetch) {
|
|
46935
|
+
const at = this.now();
|
|
46936
|
+
const entry = this.memory.get(key) ?? this.readStorage();
|
|
46937
|
+
if (entry) {
|
|
46938
|
+
const age = at - entry.fetchedAt;
|
|
46939
|
+
if (age < this.freshTtlMs) return entry.servers;
|
|
46940
|
+
if (age < this.staleCapMs) {
|
|
46941
|
+
this.startFetch(key, fetch, entry);
|
|
46942
|
+
return entry.servers;
|
|
46943
|
+
}
|
|
46944
|
+
}
|
|
46945
|
+
return this.startFetch(key, fetch, entry);
|
|
46946
|
+
}
|
|
46947
|
+
startFetch(key, fetch, stale) {
|
|
46948
|
+
const pending = this.inflight.get(key);
|
|
46949
|
+
if (pending) return pending;
|
|
46950
|
+
const run = (async () => {
|
|
46951
|
+
try {
|
|
46952
|
+
const servers = await fetch();
|
|
46953
|
+
if (servers.length > 0) {
|
|
46954
|
+
const entry = {
|
|
46955
|
+
servers,
|
|
46956
|
+
fetchedAt: this.now()
|
|
46957
|
+
};
|
|
46958
|
+
this.memory.set(key, entry);
|
|
46959
|
+
this.writeStorage(entry);
|
|
46960
|
+
}
|
|
46961
|
+
return servers;
|
|
46962
|
+
} catch {
|
|
46963
|
+
return stale?.servers;
|
|
46964
|
+
}
|
|
46965
|
+
})().finally(() => {
|
|
46966
|
+
this.inflight.delete(key);
|
|
46967
|
+
});
|
|
46968
|
+
this.inflight.set(key, run);
|
|
46969
|
+
return run;
|
|
46970
|
+
}
|
|
46971
|
+
readStorage() {
|
|
46972
|
+
if (!this.storage) return void 0;
|
|
46973
|
+
try {
|
|
46974
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
46975
|
+
if (raw === null) return void 0;
|
|
46976
|
+
const parsed = JSON.parse(raw);
|
|
46977
|
+
return isCacheEntry(parsed) ? parsed : void 0;
|
|
46978
|
+
} catch {
|
|
46979
|
+
return;
|
|
46980
|
+
}
|
|
46981
|
+
}
|
|
46982
|
+
writeStorage(entry) {
|
|
46983
|
+
if (!this.storage) return;
|
|
46984
|
+
try {
|
|
46985
|
+
this.storage.setItem(this.storageKey, JSON.stringify(entry));
|
|
46986
|
+
} catch {}
|
|
46987
|
+
}
|
|
46988
|
+
};
|
|
46989
|
+
//#endregion
|
|
46739
46990
|
//#region src/hooks/use-device-webrtc.ts
|
|
46740
46991
|
/**
|
|
46741
46992
|
* useDeviceWebrtc — WebRTC signaling hook for device-scoped streaming.
|
|
@@ -46753,15 +47004,16 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
46753
47004
|
* @param deviceId - numeric device ID (null = disabled)
|
|
46754
47005
|
* @param pollIntervalMs - how often to refresh profile slots (default: 5000)
|
|
46755
47006
|
*/
|
|
46756
|
-
/**
|
|
46757
|
-
*
|
|
46758
|
-
*
|
|
46759
|
-
|
|
46760
|
-
|
|
46761
|
-
*
|
|
46762
|
-
*
|
|
46763
|
-
*
|
|
46764
|
-
|
|
47007
|
+
/** Module-level TURN/STUN credential cache. Keyed on the caller's trpc client
|
|
47008
|
+
* object (stable per connected system — NOT on `trpc.turnProvider`, which is
|
|
47009
|
+
* a proxy minted fresh on every property access), so two admin tabs pointed
|
|
47010
|
+
* at different hubs never cross-serve credentials from the memory layer; the
|
|
47011
|
+
* storage layer is per-origin, and the page's origin IS the hub. Coalesces
|
|
47012
|
+
* concurrent fetches (the mount prefetch and the connect's own call used to
|
|
47013
|
+
* race into TWO round trips) and persists across page loads so a fresh embed
|
|
47014
|
+
* page — one per camera open on native — finds warm credentials instead of
|
|
47015
|
+
* re-paying the measured 0.2–3.6 s serialized fetch. See turn-server-cache.ts. */
|
|
47016
|
+
var turnServersCache = new TurnServerCache();
|
|
46765
47017
|
function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
|
|
46766
47018
|
const [remoteStreams, setRemoteStreams] = (0, react$1.useState)([]);
|
|
46767
47019
|
(0, react$1.useEffect)(() => {
|
|
@@ -46830,25 +47082,17 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
|
|
|
46830
47082
|
};
|
|
46831
47083
|
}, [deviceId, remoteStreams]);
|
|
46832
47084
|
const getIceServers = (0, react$1.useCallback)(async () => {
|
|
46833
|
-
|
|
46834
|
-
|
|
46835
|
-
|
|
46836
|
-
|
|
46837
|
-
const mapped = (await trpc.turnProvider.getTurnServers.query()).map((s) => {
|
|
47085
|
+
const provider = trpc.turnProvider;
|
|
47086
|
+
if (!provider) return void 0;
|
|
47087
|
+
return turnServersCache.getOrFetch(trpc, async () => {
|
|
47088
|
+
return (await provider.getTurnServers.query()).map((s) => {
|
|
46838
47089
|
return {
|
|
46839
47090
|
urls: typeof s.urls === "string" ? s.urls : [...s.urls],
|
|
46840
47091
|
...s.username !== void 0 ? { username: s.username } : {},
|
|
46841
47092
|
...s.credential !== void 0 ? { credential: s.credential } : {}
|
|
46842
47093
|
};
|
|
46843
47094
|
});
|
|
46844
|
-
|
|
46845
|
-
servers: mapped,
|
|
46846
|
-
fetchedAt: Date.now()
|
|
46847
|
-
});
|
|
46848
|
-
return mapped;
|
|
46849
|
-
} catch {
|
|
46850
|
-
return cached?.servers;
|
|
46851
|
-
}
|
|
47095
|
+
});
|
|
46852
47096
|
}, [trpc]);
|
|
46853
47097
|
(0, react$1.useEffect)(() => {
|
|
46854
47098
|
getIceServers();
|
|
@@ -47244,6 +47488,7 @@ exports.BrightnessPanel = BrightnessPanel;
|
|
|
47244
47488
|
exports.Button = Button;
|
|
47245
47489
|
exports.ButtonControl = ButtonControl;
|
|
47246
47490
|
exports.ButtonHeroCard = ButtonHeroCard;
|
|
47491
|
+
exports.CARD_MODE_MIN_COLUMNS = CARD_MODE_MIN_COLUMNS;
|
|
47247
47492
|
exports.CENTER = CENTER;
|
|
47248
47493
|
exports.CHIP_ACTIVE = CHIP_ACTIVE;
|
|
47249
47494
|
exports.CHIP_BASE = CHIP_BASE;
|
|
@@ -47395,6 +47640,7 @@ exports.PrivacyMaskSettings = PrivacyMaskSettings;
|
|
|
47395
47640
|
exports.ProviderBadge = ProviderBadge;
|
|
47396
47641
|
exports.PtzPanel = PtzPanel;
|
|
47397
47642
|
exports.QrCode = QrCode;
|
|
47643
|
+
exports.RECONNECT_POLICY = RECONNECT_POLICY;
|
|
47398
47644
|
exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
|
|
47399
47645
|
exports.RIGHT = RIGHT;
|
|
47400
47646
|
exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
|
|
@@ -47405,6 +47651,11 @@ exports.ResponseLog = ResponseLog;
|
|
|
47405
47651
|
exports.SECTION_BODY = SECTION_BODY;
|
|
47406
47652
|
exports.SECTION_CARD = SECTION_CARD;
|
|
47407
47653
|
exports.SECTION_HEADER = SECTION_HEADER;
|
|
47654
|
+
exports.SETTING_ROW = SETTING_ROW;
|
|
47655
|
+
exports.SETTING_ROW_LABEL = SETTING_ROW_LABEL;
|
|
47656
|
+
exports.SETTING_ROW_STACK_BREAKPOINT = SETTING_ROW_STACK_BREAKPOINT;
|
|
47657
|
+
exports.SETTING_ROW_VALUE = SETTING_ROW_VALUE;
|
|
47658
|
+
exports.SETTING_ROW_VALUE_TEXT = SETTING_ROW_VALUE_TEXT;
|
|
47408
47659
|
exports.SPLIT_PANEL_OUTER = SPLIT_PANEL_OUTER;
|
|
47409
47660
|
exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
|
|
47410
47661
|
exports.STACK_GAP = STACK_GAP;
|
|
@@ -47417,6 +47668,7 @@ exports.SensorHeroCard = SensorHeroCard;
|
|
|
47417
47668
|
exports.SensorInlineControl = SensorInlineControl;
|
|
47418
47669
|
exports.SensorValueAtom = SensorValueAtom;
|
|
47419
47670
|
exports.Separator = Separator;
|
|
47671
|
+
exports.SettingRow = SettingRow;
|
|
47420
47672
|
exports.Sidebar = Sidebar;
|
|
47421
47673
|
exports.SidebarItem = SidebarItem;
|
|
47422
47674
|
exports.Skeleton = Skeleton;
|
|
@@ -47522,6 +47774,7 @@ exports.metadataEntries = metadataEntries;
|
|
|
47522
47774
|
exports.metadataString = metadataString;
|
|
47523
47775
|
exports.mirror = mirror;
|
|
47524
47776
|
exports.mountAddonPage = mountAddonPage;
|
|
47777
|
+
exports.nextReconnectAction = nextReconnectAction;
|
|
47525
47778
|
exports.nextSort = nextSort;
|
|
47526
47779
|
exports.normalizeForSearch = normalizeForSearch;
|
|
47527
47780
|
exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
|
|
@@ -47535,6 +47788,7 @@ exports.resolveEventKindIcon = resolveEventKindIcon;
|
|
|
47535
47788
|
exports.resolvePrimaryChild = resolvePrimaryChild;
|
|
47536
47789
|
exports.resolveSensorDisplay = resolveSensorDisplay;
|
|
47537
47790
|
exports.resolveStepDefaultModel = resolveStepDefaultModel;
|
|
47791
|
+
exports.resolveTableLayout = resolveTableLayout;
|
|
47538
47792
|
exports.scrubReducer = scrubReducer;
|
|
47539
47793
|
exports.selectedDeviceOptions = selectedDeviceOptions;
|
|
47540
47794
|
exports.serializeRecordedCommand = serializeRecordedCommand;
|
|
@@ -48286,6 +48540,7 @@ exports.useServerManagementRollbackServerUpdate = useServerManagementRollbackSer
|
|
|
48286
48540
|
exports.useSettingsStoreCount = useSettingsStoreCount;
|
|
48287
48541
|
exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;
|
|
48288
48542
|
exports.useSettingsStoreDelete = useSettingsStoreDelete;
|
|
48543
|
+
exports.useSettingsStoreDeleteWhere = useSettingsStoreDeleteWhere;
|
|
48289
48544
|
exports.useSettingsStoreGet = useSettingsStoreGet;
|
|
48290
48545
|
exports.useSettingsStoreHistogram = useSettingsStoreHistogram;
|
|
48291
48546
|
exports.useSettingsStoreInsert = useSettingsStoreInsert;
|
|
@@ -48293,6 +48548,7 @@ exports.useSettingsStoreIsEmpty = useSettingsStoreIsEmpty;
|
|
|
48293
48548
|
exports.useSettingsStoreQuery = useSettingsStoreQuery;
|
|
48294
48549
|
exports.useSettingsStoreSet = useSettingsStoreSet;
|
|
48295
48550
|
exports.useSettingsStoreUpdate = useSettingsStoreUpdate;
|
|
48551
|
+
exports.useSettingsStoreUpdateWhere = useSettingsStoreUpdateWhere;
|
|
48296
48552
|
exports.useSmokeGetStatus = useSmokeGetStatus;
|
|
48297
48553
|
exports.useSnapshotApplyDeviceSettingsPatch = useSnapshotApplyDeviceSettingsPatch;
|
|
48298
48554
|
exports.useSnapshotGetDeviceLiveContribution = useSnapshotGetDeviceLiveContribution;
|