@camstack/ui-library 1.2.303 → 1.2.305

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/index.cjs CHANGED
@@ -10754,6 +10754,8 @@ var useDeviceHistoryListNodeSeries = trpc.deviceHistory.listNodeSeries.useQuery;
10754
10754
  var useDeviceHistoryQuery = trpc.deviceHistory.query.useQuery;
10755
10755
  /** Generated alias around `trpc.deviceHistory.queryNode.useQuery`. */
10756
10756
  var useDeviceHistoryQueryNode = trpc.deviceHistory.queryNode.useQuery;
10757
+ /** Generated alias around `trpc.deviceHistory.queryMany.useQuery`. */
10758
+ var useDeviceHistoryQueryMany = trpc.deviceHistory.queryMany.useQuery;
10757
10759
  /** Generated alias around `trpc.deviceHistory.declareSeries.useMutation`. */
10758
10760
  var useDeviceHistoryDeclareSeries = trpc.deviceHistory.declareSeries.useMutation;
10759
10761
  /** Generated alias around `trpc.deviceHistory.record.useMutation`. */
@@ -11202,8 +11204,8 @@ var useLlmMigrateRawSecrets = trpc.llm.migrateRawSecrets.useMutation;
11202
11204
  var useLlmDeleteProfile = trpc.llm.deleteProfile.useMutation;
11203
11205
  /** Generated alias around `trpc.llm.testProfile.useMutation`. */
11204
11206
  var useLlmTestProfile = trpc.llm.testProfile.useMutation;
11205
- /** Generated alias around `trpc.llm.listModels.useQuery`. */
11206
- var useLlmListModels = trpc.llm.listModels.useQuery;
11207
+ /** Generated alias around `trpc.llm.listModels.useMutation`. */
11208
+ var useLlmListModels = trpc.llm.listModels.useMutation;
11207
11209
  /** Generated alias around `trpc.llm.getDefaults.useQuery`. */
11208
11210
  var useLlmGetDefaults = trpc.llm.getDefaults.useQuery;
11209
11211
  /** Generated alias around `trpc.llm.setDefault.useMutation`. */
@@ -12584,14 +12586,6 @@ var useUserManagementValidateCredentials = trpc.userManagement.validateCredentia
12584
12586
  var useUserManagementListApiKeys = trpc.userManagement.listApiKeys.useQuery;
12585
12587
  /** Generated alias around `trpc.userManagement.revokeApiKey.useMutation`. */
12586
12588
  var useUserManagementRevokeApiKey = trpc.userManagement.revokeApiKey.useMutation;
12587
- /** Generated alias around `trpc.userManagement.createScopedToken.useMutation`. */
12588
- var useUserManagementCreateScopedToken = trpc.userManagement.createScopedToken.useMutation;
12589
- /** Generated alias around `trpc.userManagement.revokeScopedToken.useMutation`. */
12590
- var useUserManagementRevokeScopedToken = trpc.userManagement.revokeScopedToken.useMutation;
12591
- /** Generated alias around `trpc.userManagement.validateScopedToken.useQuery`. */
12592
- var useUserManagementValidateScopedToken = trpc.userManagement.validateScopedToken.useQuery;
12593
- /** Generated alias around `trpc.userManagement.listScopedTokens.useQuery`. */
12594
- var useUserManagementListScopedTokens = trpc.userManagement.listScopedTokens.useQuery;
12595
12589
  /** Generated alias around `trpc.userManagement.setupTotp.useMutation`. */
12596
12590
  var useUserManagementSetupTotp = trpc.userManagement.setupTotp.useMutation;
12597
12591
  /** Generated alias around `trpc.userManagement.confirmTotp.useMutation`. */
@@ -39840,6 +39834,202 @@ function useLiveBuffer(max) {
39840
39834
  reset
39841
39835
  ]);
39842
39836
  }
39837
+ //#endregion
39838
+ //#region src/hooks/use-debounced-string.ts
39839
+ /**
39840
+ * useDebouncedString — return `value` after `delay` ms have passed
39841
+ * without further changes. Resets the timer on every change.
39842
+ *
39843
+ * Useful for search inputs that should NOT push to URL state on every
39844
+ * keystroke. The DOM input stays controlled by an external `value`
39845
+ * (caller's `useState`); this hook produces a debounced derivation.
39846
+ */
39847
+ function useDebouncedString(value, delayMs) {
39848
+ const [debounced, setDebounced] = (0, react$1.useState)(value);
39849
+ (0, react$1.useEffect)(() => {
39850
+ const t = setTimeout(() => setDebounced(value), delayMs);
39851
+ return () => clearTimeout(t);
39852
+ }, [value, delayMs]);
39853
+ return debounced;
39854
+ }
39855
+ //#endregion
39856
+ //#region src/composites/log-history-paging.ts
39857
+ /**
39858
+ * Accumulate slices until `limit` entries, `done`, or an abort. An abort keeps
39859
+ * what was already collected and the cursor reached, so "Load older" resumes
39860
+ * where the scan stopped instead of starting again.
39861
+ */
39862
+ async function collectPage(options) {
39863
+ const { fetchSlice, limit, signal, onProgress } = options;
39864
+ const entries = [];
39865
+ let cursor = options.before;
39866
+ let scannedBytes = 0;
39867
+ let source;
39868
+ for (;;) {
39869
+ if (signal.aborted) return {
39870
+ entries,
39871
+ nextBefore: cursor ?? null,
39872
+ done: false,
39873
+ cancelled: true,
39874
+ scannedBytes,
39875
+ source
39876
+ };
39877
+ let page;
39878
+ try {
39879
+ page = await fetchSlice(cursor, Math.max(1, limit - entries.length), signal);
39880
+ } catch (error) {
39881
+ if (signal.aborted) return {
39882
+ entries,
39883
+ nextBefore: cursor ?? null,
39884
+ done: false,
39885
+ cancelled: true,
39886
+ scannedBytes,
39887
+ source
39888
+ };
39889
+ throw error;
39890
+ }
39891
+ entries.push(...page.entries);
39892
+ scannedBytes += page.scannedBytes;
39893
+ source = page.source;
39894
+ onProgress?.(scannedBytes);
39895
+ const exhausted = page.done || page.nextBefore === null;
39896
+ if (page.cancelled === true) return {
39897
+ entries,
39898
+ nextBefore: page.nextBefore,
39899
+ done: exhausted,
39900
+ cancelled: true,
39901
+ scannedBytes,
39902
+ source
39903
+ };
39904
+ if (exhausted || entries.length >= limit) return {
39905
+ entries,
39906
+ nextBefore: page.nextBefore,
39907
+ done: exhausted,
39908
+ cancelled: false,
39909
+ scannedBytes,
39910
+ source
39911
+ };
39912
+ cursor = page.nextBefore ?? void 0;
39913
+ }
39914
+ }
39915
+ /** Stable identity of an entry: the server id, else the legacy composite key. */
39916
+ function logEntryKey(entry) {
39917
+ if (entry.id !== void 0 && entry.id !== "") return `id:${entry.id}`;
39918
+ return `${entry.timestamp}|${entry.level}|${entry.scope ?? ""}|${entry.message}`;
39919
+ }
39920
+ /**
39921
+ * History (newest first, as paged) merged with live entries (arrival order),
39922
+ * deduped by key — a live entry wins (richer tags/meta) — and returned
39923
+ * chronologically, newest LAST. Ties keep insertion order.
39924
+ */
39925
+ function mergeLogEntries(history, live) {
39926
+ const byKey = /* @__PURE__ */ new Map();
39927
+ for (const entry of history) byKey.set(logEntryKey(entry), entry);
39928
+ for (const entry of live) byKey.set(logEntryKey(entry), entry);
39929
+ return [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
39930
+ }
39931
+ //#endregion
39932
+ //#region src/hooks/use-log-history.ts
39933
+ /**
39934
+ * useLogHistory — pages the server-served log history (D898).
39935
+ *
39936
+ * The first page and every older page go through `collectPage`, which keeps
39937
+ * calling `logs.query` with `before: nextBefore` while the server answers a
39938
+ * short, not-done slice. Changing the filters aborts whatever is in flight and
39939
+ * restarts from the first page; `cancel()` aborts the scan but keeps what was
39940
+ * found and the cursor reached, so "Load older" resumes from there.
39941
+ */
39942
+ var INITIAL = {
39943
+ entries: [],
39944
+ nextBefore: null,
39945
+ done: false,
39946
+ status: "loading",
39947
+ scannedBytes: 0,
39948
+ source: void 0,
39949
+ error: null
39950
+ };
39951
+ function useLogHistory(query, limit) {
39952
+ const utils = trpc.useUtils();
39953
+ const [state, setState] = (0, react$1.useState)(INITIAL);
39954
+ const stateRef = (0, react$1.useRef)(state);
39955
+ stateRef.current = state;
39956
+ const queryRef = (0, react$1.useRef)(query);
39957
+ queryRef.current = query;
39958
+ const abortRef = (0, react$1.useRef)(null);
39959
+ const queryKey = JSON.stringify(query);
39960
+ const run = (0, react$1.useCallback)(async (before, reset) => {
39961
+ abortRef.current?.abort();
39962
+ const controller = new AbortController();
39963
+ abortRef.current = controller;
39964
+ setState((prev) => ({
39965
+ ...reset ? INITIAL : prev,
39966
+ status: "loading",
39967
+ scannedBytes: 0,
39968
+ error: null
39969
+ }));
39970
+ const fetchSlice = (cursor, remaining, signal) => utils.client.logs.query.query({
39971
+ ...queryRef.current,
39972
+ limit: remaining,
39973
+ ...cursor !== void 0 ? { before: cursor } : {}
39974
+ }, { signal });
39975
+ try {
39976
+ const out = await collectPage({
39977
+ fetchSlice,
39978
+ limit,
39979
+ ...before !== void 0 ? { before } : {},
39980
+ signal: controller.signal,
39981
+ onProgress: (scannedBytes) => {
39982
+ if (abortRef.current === controller) setState((prev) => ({
39983
+ ...prev,
39984
+ scannedBytes
39985
+ }));
39986
+ }
39987
+ });
39988
+ if (abortRef.current !== controller) return;
39989
+ setState((prev) => ({
39990
+ entries: reset ? out.entries : [...prev.entries, ...out.entries],
39991
+ nextBefore: out.nextBefore,
39992
+ done: out.done,
39993
+ status: "idle",
39994
+ scannedBytes: out.scannedBytes,
39995
+ source: out.source ?? prev.source,
39996
+ error: null
39997
+ }));
39998
+ } catch (error) {
39999
+ if (abortRef.current !== controller) return;
40000
+ setState((prev) => ({
40001
+ ...prev,
40002
+ status: "idle",
40003
+ error: error instanceof Error ? error.message : String(error)
40004
+ }));
40005
+ }
40006
+ }, [utils, limit]);
40007
+ (0, react$1.useEffect)(() => {
40008
+ run(void 0, true);
40009
+ return () => {
40010
+ abortRef.current?.abort();
40011
+ abortRef.current = null;
40012
+ };
40013
+ }, [run, queryKey]);
40014
+ const loadOlder = (0, react$1.useCallback)(() => {
40015
+ const current = stateRef.current;
40016
+ if (current.status === "loading" || current.done) return;
40017
+ run(current.nextBefore ?? void 0, false);
40018
+ }, [run]);
40019
+ const cancel = (0, react$1.useCallback)(() => {
40020
+ abortRef.current?.abort();
40021
+ }, []);
40022
+ return {
40023
+ entries: state.entries,
40024
+ status: state.status,
40025
+ done: state.done,
40026
+ scannedBytes: state.scannedBytes,
40027
+ source: state.source,
40028
+ error: state.error,
40029
+ loadOlder,
40030
+ cancel
40031
+ };
40032
+ }
39843
40033
  /**
39844
40034
  * True when the viewport is scrolled to (or within `threshold` px of) the
39845
40035
  * bottom of its content.
@@ -39896,12 +40086,6 @@ var LEVEL_STYLES = {
39896
40086
  color: "text-gray-500"
39897
40087
  }
39898
40088
  };
39899
- var LEVEL_SEVERITY = {
39900
- debug: 0,
39901
- info: 1,
39902
- warn: 2,
39903
- error: 3
39904
- };
39905
40089
  var LEVEL_OPTIONS = [
39906
40090
  "debug",
39907
40091
  "info",
@@ -39910,15 +40094,18 @@ var LEVEL_OPTIONS = [
39910
40094
  ];
39911
40095
  /** Max live entries kept in the ring buffer. */
39912
40096
  var MAX_LIVE_ENTRIES$1 = 500;
39913
- function passesLevel(logLevel, filterLevel) {
39914
- if (!filterLevel) return true;
39915
- return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
39916
- }
39917
- function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight = "max-h-96", showScope = false, showDeviceChip = false, showFilters = true, limit = 100, liveBuffer: externalBuffer, onClose, className }) {
40097
+ /** Text filter debounce before it goes to the server. */
40098
+ var TEXT_DEBOUNCE_MS = 300;
40099
+ /** Distance from the top (px) that asks for the next older page. */
40100
+ var TOP_LOAD_THRESHOLD_PX = 24;
40101
+ var BYTES_PER_MB = 1024 * 1024;
40102
+ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight = "max-h-96", showScope = false, showDeviceChip = false, showFilters = true, showSourceSelect, limit = 100, liveBuffer: externalBuffer, onClose, className }) {
39918
40103
  const t = useUiText();
40104
+ const sourceSelectOn = showSourceSelect ?? showFilters;
39919
40105
  const [localAddonId, setLocalAddonId] = (0, react$1.useState)("");
39920
40106
  const [localDeviceId, setLocalDeviceId] = (0, react$1.useState)("");
39921
40107
  const [levelFilter, setLevelFilter] = (0, react$1.useState)(initialLevel ?? "info");
40108
+ const [sourceChoice, setSourceChoice] = (0, react$1.useState)(void 0);
39922
40109
  const agentId = propsAgentId;
39923
40110
  const addonId = propsAddonId ?? (localAddonId || void 0);
39924
40111
  const deviceId = propsDeviceId ?? (localDeviceId ? Number(localDeviceId) : void 0);
@@ -39963,48 +40150,63 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
39963
40150
  setExpandAll(false);
39964
40151
  }
39965
40152
  }, [scopeKey, buffer]);
39966
- const { data: initialLogs, isLoading } = trpc.logs.query.useQuery({
39967
- ...tags ? { tags } : {},
39968
- limit
39969
- }, { staleTime: 3e4 });
39970
- trpc.logs.subscribe.useSubscription(tags ? { tags } : {}, { onData: (entry) => {
40153
+ const debouncedText = useDebouncedString(searchText, TEXT_DEBOUNCE_MS).trim();
40154
+ const sourcesQuery = trpc.logs.sources.useQuery(void 0, { staleTime: 6e4 });
40155
+ const readableSources = (0, react$1.useMemo)(() => (sourcesQuery.data ?? []).filter((source) => source.readable), [sourcesQuery.data]);
40156
+ const history = useLogHistory((0, react$1.useMemo)(() => ({
40157
+ ...sourceChoice ? { source: sourceChoice } : {},
40158
+ level: levelFilter,
40159
+ ...debouncedText ? { text: debouncedText } : {},
40160
+ ...tags ? { tags } : {}
40161
+ }), [
40162
+ sourceChoice,
40163
+ levelFilter,
40164
+ debouncedText,
40165
+ tags
40166
+ ]), limit);
40167
+ trpc.logs.subscribe.useSubscription({
40168
+ level: levelFilter,
40169
+ ...debouncedText ? { text: debouncedText } : {},
40170
+ ...tags ? { tags } : {}
40171
+ }, { onData: (entry) => {
39971
40172
  buffer.append(entry);
39972
40173
  } });
40174
+ const filterKey = `${sourceChoice ?? ""}|${levelFilter}|${debouncedText}`;
40175
+ const prevFilterRef = (0, react$1.useRef)(filterKey);
40176
+ (0, react$1.useEffect)(() => {
40177
+ if (prevFilterRef.current === filterKey) return;
40178
+ prevFilterRef.current = filterKey;
40179
+ buffer.reset();
40180
+ setFollow(true);
40181
+ }, [filterKey, buffer]);
39973
40182
  const allLogs = (0, react$1.useMemo)(() => {
39974
- const initial = (initialLogs ?? []).map((log) => ({
39975
- timestamp: log.timestamp,
39976
- level: log.level,
39977
- message: log.message,
39978
- scope: log.scope,
39979
- tags: log.tags,
39980
- meta: log.meta
39981
- }));
39982
- const byKey = /* @__PURE__ */ new Map();
39983
- for (const log of initial) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
39984
- for (const log of liveLogs) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
39985
- const merged = [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
39986
- const levelFiltered = (clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged).filter((log) => passesLevel(log.level, levelFilter));
39987
- const needle = searchText.trim().toLowerCase();
39988
- if (!needle) return levelFiltered;
39989
- return levelFiltered.filter((log) => {
39990
- if (log.message.toLowerCase().includes(needle)) return true;
39991
- if (log.scope && log.scope.toLowerCase().includes(needle)) return true;
39992
- if (log.tags) {
39993
- for (const v of Object.values(log.tags)) if (v !== void 0 && String(v).toLowerCase().includes(needle)) return true;
39994
- }
39995
- if (log.meta) for (const v of Object.values(log.meta)) {
39996
- if (v === void 0 || v === null) continue;
39997
- if (formatMetaValue(v).toLowerCase().includes(needle)) return true;
39998
- }
39999
- return false;
40000
- });
40183
+ const merged = mergeLogEntries(history.entries, liveLogs);
40184
+ return clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged;
40001
40185
  }, [
40002
- initialLogs,
40186
+ history.entries,
40003
40187
  liveLogs,
40004
- levelFilter,
40005
- clearedAt,
40006
- searchText
40188
+ clearedAt
40007
40189
  ]);
40190
+ const prependAnchorRef = (0, react$1.useRef)(null);
40191
+ const requestOlder = (0, react$1.useCallback)(() => {
40192
+ if (history.status === "loading" || history.done) return;
40193
+ const el = scrollRef.current;
40194
+ if (el) prependAnchorRef.current = {
40195
+ height: el.scrollHeight,
40196
+ top: el.scrollTop
40197
+ };
40198
+ history.loadOlder();
40199
+ }, [history]);
40200
+ (0, react$1.useLayoutEffect)(() => {
40201
+ const anchor = prependAnchorRef.current;
40202
+ const el = scrollRef.current;
40203
+ if (!anchor || !el || history.status !== "idle") return;
40204
+ prependAnchorRef.current = null;
40205
+ const next = anchor.top + (el.scrollHeight - anchor.height);
40206
+ if (Math.abs(next - el.scrollTop) < 1) return;
40207
+ programmaticScrollRef.current = true;
40208
+ el.scrollTop = next;
40209
+ }, [history.status, history.entries.length]);
40008
40210
  const scrollToBottom = (0, react$1.useCallback)((behavior) => {
40009
40211
  const el = scrollRef.current;
40010
40212
  if (!el) return;
@@ -40015,15 +40217,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
40015
40217
  });
40016
40218
  }, []);
40017
40219
  const [hasNewWhilePaused, setHasNewWhilePaused] = (0, react$1.useState)(false);
40018
- const prevLenRef = (0, react$1.useRef)(allLogs.length);
40220
+ const lastKey = allLogs.length > 0 ? logEntryKey(allLogs[allLogs.length - 1]) : "";
40221
+ const prevLastKeyRef = (0, react$1.useRef)(lastKey);
40019
40222
  (0, react$1.useLayoutEffect)(() => {
40020
- const grew = allLogs.length > prevLenRef.current;
40021
- prevLenRef.current = allLogs.length;
40022
- if (!grew) return;
40223
+ const newestChanged = lastKey !== "" && lastKey !== prevLastKeyRef.current;
40224
+ prevLastKeyRef.current = lastKey;
40225
+ if (!newestChanged) return;
40023
40226
  if (follow) scrollToBottom();
40024
40227
  else setHasNewWhilePaused(true);
40025
40228
  }, [
40026
- allLogs.length,
40229
+ lastKey,
40027
40230
  follow,
40028
40231
  scrollToBottom
40029
40232
  ]);
@@ -40043,8 +40246,9 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
40043
40246
  if (!el) return;
40044
40247
  const pinned = isPinnedToBottom(el, 40);
40045
40248
  setFollow((prev) => prev === pinned ? prev : pinned);
40046
- }, []);
40047
- const expandableKeys = (0, react$1.useMemo)(() => allLogs.filter((log) => log.tags && Object.keys(log.tags).length > 0 || log.meta && Object.keys(log.meta).length > 0).map((log) => `${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`), [allLogs]);
40249
+ if (el.scrollTop <= TOP_LOAD_THRESHOLD_PX) requestOlder();
40250
+ }, [requestOlder]);
40251
+ const expandableKeys = (0, react$1.useMemo)(() => allLogs.filter((log) => log.tags && Object.keys(log.tags).length > 0 || log.meta && Object.keys(log.meta).length > 0).map(logEntryKey), [allLogs]);
40048
40252
  const toggleExpandAll = (0, react$1.useCallback)(() => {
40049
40253
  setExpandAll((prev) => {
40050
40254
  if (prev) setExpandedRows(/* @__PURE__ */ new Set());
@@ -40091,7 +40295,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
40091
40295
  setTimeout(() => setCopiedRowKey((prev) => prev === key ? null : prev), 1200);
40092
40296
  });
40093
40297
  }, []);
40094
- const rowKey = (0, react$1.useCallback)((log) => `${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, []);
40298
+ const rowKey = logEntryKey;
40095
40299
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40096
40300
  className: cn("h-full min-h-0 flex flex-col", className),
40097
40301
  onClick: (e) => e.stopPropagation(),
@@ -40126,6 +40330,23 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
40126
40330
  children: [l, "+"]
40127
40331
  }, l))
40128
40332
  }),
40333
+ sourceSelectOn && readableSources.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40334
+ className: "w-36",
40335
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
40336
+ "aria-label": t("uiLibrary.logStream.source", "Log source"),
40337
+ className: "h-5 text-[10px]",
40338
+ value: sourceChoice ?? readableSources.find((x) => x.isDefault)?.addonId ?? "",
40339
+ onChange: (e) => setSourceChoice(e.target.value),
40340
+ options: readableSources.map((x) => ({
40341
+ value: x.addonId,
40342
+ label: x.addonId
40343
+ }))
40344
+ })
40345
+ }),
40346
+ sourceSelectOn && readableSources.length <= 1 && history.source && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
40347
+ className: "text-[9px] text-foreground-subtle",
40348
+ children: t("uiLibrary.logStream.sourceName", "Source: {{name}}", { name: history.source })
40349
+ }),
40129
40350
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
40130
40351
  icon: ArrowDownToLine,
40131
40352
  label: follow ? t("uiLibrary.logStream.following", "Following") : t("uiLibrary.logStream.paused", "Paused"),
@@ -40224,11 +40445,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
40224
40445
  onScroll: handleScroll,
40225
40446
  className: cn("flex-1 min-h-0 overflow-auto font-mono text-[10px]", maxHeight),
40226
40447
  children: [
40227
- isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40228
- className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
40229
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), t("uiLibrary.logStream.loading", "Loading logs...")]
40448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryEdge, {
40449
+ status: history.status,
40450
+ done: history.done,
40451
+ scannedBytes: history.scannedBytes,
40452
+ hasRows: allLogs.length > 0,
40453
+ error: history.error,
40454
+ onLoadOlder: requestOlder,
40455
+ onCancel: history.cancel
40230
40456
  }),
40231
- !isLoading && allLogs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40457
+ history.status === "idle" && history.done && allLogs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40232
40458
  className: "py-4 text-center text-foreground-subtle",
40233
40459
  children: t("uiLibrary.logStream.noLogs", "No logs found")
40234
40460
  }),
@@ -40352,6 +40578,49 @@ function DeviceChip({ deviceId, deviceName }) {
40352
40578
  children: label
40353
40579
  });
40354
40580
  }
40581
+ /**
40582
+ * The top of the list: the state of the older-history scan. A scan that is
40583
+ * still looking (short page, source not done) says how much it has read and
40584
+ * can be cancelled; the end of the source is stated quietly.
40585
+ */
40586
+ function HistoryEdge({ status, done, scannedBytes, hasRows, error, onLoadOlder, onCancel }) {
40587
+ const t = useUiText();
40588
+ if (status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40589
+ className: "flex items-center justify-center gap-2 py-2 text-foreground-subtle",
40590
+ children: [
40591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }),
40592
+ scannedBytes > 0 ? t("uiLibrary.logStream.searching", "Searching older logs… ({{mb}} MB scanned)", { mb: (scannedBytes / BYTES_PER_MB).toFixed(1) }) : t("uiLibrary.logStream.loading", "Loading logs..."),
40593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
40594
+ variant: "outline",
40595
+ size: "sm",
40596
+ onClick: onCancel,
40597
+ children: t("uiLibrary.logStream.cancel", "Cancel")
40598
+ })
40599
+ ]
40600
+ });
40601
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40602
+ className: "flex items-center justify-center gap-2 py-2 text-danger",
40603
+ children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
40604
+ variant: "outline",
40605
+ size: "sm",
40606
+ onClick: onLoadOlder,
40607
+ children: t("uiLibrary.logStream.retry", "Retry")
40608
+ })]
40609
+ });
40610
+ if (done) return hasRows ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40611
+ className: "py-2 text-center text-foreground-subtle",
40612
+ children: t("uiLibrary.logStream.beginning", "Beginning of logs")
40613
+ }) : null;
40614
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40615
+ className: "flex justify-center py-1.5",
40616
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
40617
+ variant: "outline",
40618
+ size: "sm",
40619
+ onClick: onLoadOlder,
40620
+ children: t("uiLibrary.logStream.loadOlder", "Load older")
40621
+ })
40622
+ });
40623
+ }
40355
40624
  function ScopeBadge$2({ label, value }) {
40356
40625
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40357
40626
  className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
@@ -52395,24 +52664,6 @@ function DeviceBatchActionsBar(props) {
52395
52664
  })] });
52396
52665
  }
52397
52666
  //#endregion
52398
- //#region src/hooks/use-debounced-string.ts
52399
- /**
52400
- * useDebouncedString — return `value` after `delay` ms have passed
52401
- * without further changes. Resets the timer on every change.
52402
- *
52403
- * Useful for search inputs that should NOT push to URL state on every
52404
- * keystroke. The DOM input stays controlled by an external `value`
52405
- * (caller's `useState`); this hook produces a debounced derivation.
52406
- */
52407
- function useDebouncedString(value, delayMs) {
52408
- const [debounced, setDebounced] = (0, react$1.useState)(value);
52409
- (0, react$1.useEffect)(() => {
52410
- const t = setTimeout(() => setDebounced(value), delayMs);
52411
- return () => clearTimeout(t);
52412
- }, [value, delayMs]);
52413
- return debounced;
52414
- }
52415
- //#endregion
52416
52667
  //#region src/hooks/use-persisted-page-size.ts
52417
52668
  /**
52418
52669
  * usePersistedPageSize — a "rows per page" choice remembered per viewer.
@@ -58052,6 +58303,32 @@ function FormField$1({ label, description, error, required, children, orientatio
58052
58303
  });
58053
58304
  }
58054
58305
  //#endregion
58306
+ //#region src/composites/grouped-tile-picker.tsx
58307
+ function GroupedTilePicker({ groups, onPick, emptyMessage }) {
58308
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58309
+ className: "space-y-4",
58310
+ children: [groups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58311
+ className: "text-xs text-foreground-subtle",
58312
+ children: emptyMessage
58313
+ }), groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58314
+ className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
58315
+ children: group.label
58316
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58317
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-3",
58318
+ children: group.tiles.map((tile) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
58319
+ type: "button",
58320
+ "data-testid": tile.testId,
58321
+ onClick: () => onPick(tile.key),
58322
+ className: "flex flex-col items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-3 text-xs transition-colors hover:border-primary/50 hover:bg-surface-hover",
58323
+ children: [tile.icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58324
+ className: "text-center font-medium text-foreground",
58325
+ children: tile.label
58326
+ })]
58327
+ }, tile.key))
58328
+ })] }, group.id))]
58329
+ });
58330
+ }
58331
+ //#endregion
58055
58332
  //#region src/composites/hls-quality.ts
58056
58333
  /**
58057
58334
  * Pure helpers for the HLS playback quality selector.
@@ -60392,7 +60669,7 @@ function ResponseLog({ data, label = "Response", className = "" }) {
60392
60669
  * Two consumers: the user-scope editor on `/system/users` (admin grants
60393
60670
  * a baseline scope set to a non-admin user) and the scoped-token create
60394
60671
  * modal on `/system/api-keys` (a caller carves out a subset of THEIR
60395
- * scopes for a `cst_*` token).
60672
+ * scopes for a token).
60396
60673
  *
60397
60674
  * Row shapes per scope `type`:
60398
60675
  * - category → target: 'device' | 'system'
@@ -64863,6 +65140,7 @@ exports.Grid2x2 = Grid2x2;
64863
65140
  exports.GripHorizontal = GripHorizontal;
64864
65141
  exports.GripTrack = GripTrack;
64865
65142
  exports.GroupedModelSelector = GroupedModelSelector;
65143
+ exports.GroupedTilePicker = GroupedTilePicker;
64866
65144
  exports.HOST_WIDGETS = HOST_WIDGETS;
64867
65145
  exports.HUB_SESSION_RUN_MS = HUB_SESSION_RUN_MS;
64868
65146
  exports.Hammer = Hammer;
@@ -65684,6 +65962,7 @@ exports.useDeviceHistoryGetStatus = useDeviceHistoryGetStatus;
65684
65962
  exports.useDeviceHistoryListNodeSeries = useDeviceHistoryListNodeSeries;
65685
65963
  exports.useDeviceHistoryListSeries = useDeviceHistoryListSeries;
65686
65964
  exports.useDeviceHistoryQuery = useDeviceHistoryQuery;
65965
+ exports.useDeviceHistoryQueryMany = useDeviceHistoryQueryMany;
65687
65966
  exports.useDeviceHistoryQueryNode = useDeviceHistoryQueryNode;
65688
65967
  exports.useDeviceHistoryRecord = useDeviceHistoryRecord;
65689
65968
  exports.useDeviceHistoryRecordBuckets = useDeviceHistoryRecordBuckets;
@@ -66639,7 +66918,6 @@ exports.useUpdateGetStatus = useUpdateGetStatus;
66639
66918
  exports.useUpdateInstallUpdate = useUpdateInstallUpdate;
66640
66919
  exports.useUrlQueryParam = useUrlQueryParam;
66641
66920
  exports.useUserManagementConfirmTotp = useUserManagementConfirmTotp;
66642
- exports.useUserManagementCreateScopedToken = useUserManagementCreateScopedToken;
66643
66921
  exports.useUserManagementCreateUser = useUserManagementCreateUser;
66644
66922
  exports.useUserManagementDeleteUser = useUserManagementDeleteUser;
66645
66923
  exports.useUserManagementDisableTotp = useUserManagementDisableTotp;
@@ -66648,7 +66926,6 @@ exports.useUserManagementGetTotpStatus = useUserManagementGetTotpStatus;
66648
66926
  exports.useUserManagementListApiKeys = useUserManagementListApiKeys;
66649
66927
  exports.useUserManagementListExternalIdentities = useUserManagementListExternalIdentities;
66650
66928
  exports.useUserManagementListOauthSessions = useUserManagementListOauthSessions;
66651
- exports.useUserManagementListScopedTokens = useUserManagementListScopedTokens;
66652
66929
  exports.useUserManagementListUsers = useUserManagementListUsers;
66653
66930
  exports.useUserManagementMigrateRawSecrets = useUserManagementMigrateRawSecrets;
66654
66931
  exports.useUserManagementOauthExchangeCode = useUserManagementOauthExchangeCode;
@@ -66658,13 +66935,11 @@ exports.useUserManagementOauthVerifyAccessToken = useUserManagementOauthVerifyAc
66658
66935
  exports.useUserManagementResetPassword = useUserManagementResetPassword;
66659
66936
  exports.useUserManagementRevokeApiKey = useUserManagementRevokeApiKey;
66660
66937
  exports.useUserManagementRevokeOauthSession = useUserManagementRevokeOauthSession;
66661
- exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
66662
66938
  exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
66663
66939
  exports.useUserManagementSetupTotp = useUserManagementSetupTotp;
66664
66940
  exports.useUserManagementUnlinkExternalIdentity = useUserManagementUnlinkExternalIdentity;
66665
66941
  exports.useUserManagementUpdateUser = useUserManagementUpdateUser;
66666
66942
  exports.useUserManagementValidateCredentials = useUserManagementValidateCredentials;
66667
- exports.useUserManagementValidateScopedToken = useUserManagementValidateScopedToken;
66668
66943
  exports.useUserManagementVerifyTotp = useUserManagementVerifyTotp;
66669
66944
  exports.useVacuumControlGetStatus = useVacuumControlGetStatus;
66670
66945
  exports.useVacuumControlLocate = useVacuumControlLocate;