mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-08 15:52:48 +03:00
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* fix semantic search reindex sqlite-vec added the `_info` shadow table in 0.1.6 and drops it unconditionally when a vec0 table is destroyed, so `DROP TABLE` on a table written by 0.17 failed with "SQL logic error" once 0.18 moved to 0.1.9. `SqliteQueueDatabase` queues non-SELECT statements and stores the exception on the cursor it returns, and nothing read those cursors, so the failed drop and every write after it went unreported while reindex still logged "Embedded N thumbnails". `drop_embeddings_tables()` now recreates the missing `_info` stub before dropping, and writes go through `execute_write()`, which waits on the cursor so failures raise. `INSERT OR REPLACE` is gone too, since vec0 implements neither REPLACE nor UPSERT and it always failed on an id already in the table, including under the 0.1.3 build 0.17 shipped. * use lock * show reindex failure in status bar
213 lines
5.9 KiB
TypeScript
213 lines
5.9 KiB
TypeScript
import NavItem from "./NavItem";
|
|
import { IoIosWarning } from "react-icons/io";
|
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|
import useSWR from "swr";
|
|
import { FrigateStats } from "@/types/stats";
|
|
import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws";
|
|
import {
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import useStats from "@/hooks/use-stats";
|
|
import GeneralSettings from "../menu/GeneralSettings";
|
|
import useNavigation from "@/hooks/use-navigation";
|
|
import {
|
|
StatusBarMessagesContext,
|
|
StatusMessage,
|
|
} from "@/context/statusbar-provider";
|
|
import { Link } from "react-router-dom";
|
|
import { cn } from "@/lib/utils";
|
|
import { isMobile } from "react-device-detect";
|
|
import { isPWA } from "@/utils/isPWA";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
function Bottombar() {
|
|
const navItems = useNavigation("secondary");
|
|
|
|
// Render 48px touch targets when they fit with even spacing, otherwise fall
|
|
// back to the compact size. Measured against the live bar width and icon
|
|
// count (which varies with enabled nav items and the status alert).
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const [large, setLarge] = useState(false);
|
|
|
|
useLayoutEffect(() => {
|
|
const el = containerRef.current;
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
const TARGET = 48; // standard bottom-nav touch target (px)
|
|
const MIN_GAP = 8; // minimum spacing between targets (px)
|
|
|
|
const compute = () => {
|
|
const count = el.children.length;
|
|
if (count === 0) {
|
|
return;
|
|
}
|
|
const needed = count * TARGET + Math.max(count - 1, 0) * MIN_GAP;
|
|
setLarge(needed <= el.clientWidth);
|
|
};
|
|
|
|
compute();
|
|
|
|
const resize = new ResizeObserver(compute);
|
|
resize.observe(el);
|
|
// recompute when items are added/removed (e.g. the status alert appears)
|
|
const mutation = new MutationObserver(compute);
|
|
mutation.observe(el, { childList: true });
|
|
|
|
return () => {
|
|
resize.disconnect();
|
|
mutation.disconnect();
|
|
};
|
|
}, [navItems]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={cn(
|
|
"absolute inset-x-4 bottom-0 flex h-16 flex-row items-center justify-between",
|
|
isMobile &&
|
|
(isPWA
|
|
? "h-[calc(3rem+env(safe-area-inset-bottom))] pb-[env(safe-area-inset-bottom)] md:h-[calc(4rem+env(safe-area-inset-bottom))]"
|
|
: "h-12 md:h-16 md:pb-2"),
|
|
)}
|
|
>
|
|
{navItems.map((item) => (
|
|
<NavItem
|
|
key={item.id}
|
|
large={large}
|
|
className="p-2"
|
|
item={item}
|
|
Icon={item.icon}
|
|
/>
|
|
))}
|
|
<GeneralSettings large={large} className="p-2" />
|
|
<StatusAlertNav large={large} className="p-2" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type StatusAlertNavProps = {
|
|
className?: string;
|
|
large?: boolean;
|
|
};
|
|
function StatusAlertNav({ className, large }: StatusAlertNavProps) {
|
|
const { t } = useTranslation(["views/system"]);
|
|
const { data: initialStats } = useSWR<FrigateStats>("stats", {
|
|
revalidateOnFocus: false,
|
|
});
|
|
const latestStats = useFrigateStats();
|
|
|
|
const { messages, addMessage, clearMessages } = useContext(
|
|
StatusBarMessagesContext,
|
|
)!;
|
|
|
|
const stats = useMemo(() => {
|
|
if (latestStats) {
|
|
return latestStats;
|
|
}
|
|
|
|
return initialStats;
|
|
}, [initialStats, latestStats]);
|
|
const { potentialProblems } = useStats(stats);
|
|
|
|
useEffect(() => {
|
|
clearMessages("stats");
|
|
potentialProblems.forEach((problem) => {
|
|
addMessage(
|
|
"stats",
|
|
problem.text,
|
|
problem.color,
|
|
undefined,
|
|
problem.relevantLink,
|
|
);
|
|
});
|
|
}, [potentialProblems, addMessage, clearMessages]);
|
|
|
|
const { payload: reindexState } = useEmbeddingsReindexProgress();
|
|
|
|
useEffect(() => {
|
|
if (reindexState) {
|
|
clearMessages("embeddings-reindex");
|
|
|
|
if (reindexState.status === "indexing") {
|
|
addMessage(
|
|
"embeddings-reindex",
|
|
t("stats.reindexingEmbeddings", {
|
|
processed: Math.floor(
|
|
(reindexState.processed_objects / reindexState.total_objects) *
|
|
100,
|
|
),
|
|
}),
|
|
);
|
|
} else if (reindexState.status === "failed") {
|
|
addMessage("embeddings-reindex", t("stats.reindexEmbeddingsFailed"));
|
|
}
|
|
}
|
|
}, [reindexState, addMessage, clearMessages, t]);
|
|
|
|
if (!messages || Object.keys(messages).length === 0) {
|
|
return;
|
|
}
|
|
|
|
return (
|
|
<Drawer>
|
|
<DrawerTrigger asChild>
|
|
<div
|
|
className={cn(
|
|
"flex flex-col items-center justify-center p-2",
|
|
large && "size-12",
|
|
)}
|
|
>
|
|
<IoIosWarning
|
|
className={cn(
|
|
"text-danger md:m-[6px]",
|
|
large ? "size-6" : "size-5",
|
|
)}
|
|
/>
|
|
</div>
|
|
</DrawerTrigger>
|
|
<DrawerContent
|
|
className={cn(
|
|
"mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden px-2 py-4">
|
|
{Object.entries(messages).map(([key, messageArray]) => (
|
|
<div key={key} className="flex w-full items-center gap-2">
|
|
{messageArray.map(({ id, text, color, link }: StatusMessage) => {
|
|
const message = (
|
|
<div key={id} className="flex items-center gap-2 text-xs">
|
|
<IoIosWarning
|
|
className={`size-5 ${color || "text-danger"}`}
|
|
/>
|
|
{text}
|
|
</div>
|
|
);
|
|
|
|
if (link) {
|
|
return (
|
|
<Link key={id} to={link}>
|
|
{message}
|
|
</Link>
|
|
);
|
|
} else {
|
|
return message;
|
|
}
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
);
|
|
}
|
|
|
|
export default Bottombar;
|