Files
frigate/web/src/components/Statusbar.tsx
T
Josh HawkinsandGitHub 26e6adee88
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 (#24407)
* 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
2026-09-19 08:10:36 -06:00

227 lines
7.0 KiB
TypeScript

import { useEmbeddingsReindexProgress } from "@/api/ws";
import {
StatusBarMessagesContext,
StatusMessage,
} from "@/context/statusbar-provider";
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
import { cn } from "@/lib/utils";
import type { ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { useContext, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md";
import { Link } from "react-router-dom";
export default function Statusbar() {
const { t } = useTranslation(["views/system"]);
const isAdmin = useIsAdmin();
const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;
const stats = useAutoFrigateStats();
const cpuPercent = useMemo(() => {
const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu;
if (!systemCpu || systemCpu == "0.0") {
return null;
}
return parseInt(systemCpu);
}, [stats]);
const { potentialProblems } = useStats(stats);
useEffect(() => {
clearMessages("stats");
potentialProblems.forEach((problem) => {
addMessage(
"stats",
problem.text,
problem.color,
undefined,
problem.relevantLink,
);
});
}, [potentialProblems, addMessage, clearMessages]);
const { data: profilesData } = useSWR<ProfilesApiResponse>("profiles");
const activeProfile = useMemo(() => {
if (!profilesData?.active_profile || !profilesData.profiles) return null;
const info = profilesData.profiles.find(
(p) => p.name === profilesData.active_profile,
);
const allNames = profilesData.profiles.map((p) => p.name).sort();
return {
name: profilesData.active_profile,
friendlyName: info?.friendly_name ?? profilesData.active_profile,
color: getProfileColor(profilesData.active_profile, allNames),
};
}, [profilesData]);
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]);
return (
<div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground">
<div className="flex h-full items-center gap-2">
{cpuPercent && (
<Link to="/system#general">
<div className="flex cursor-pointer items-center gap-2 text-sm hover:underline">
<MdCircle
className={`size-2 ${
cpuPercent < 50
? "text-success"
: cpuPercent < 80
? "text-orange-400"
: "text-danger"
}`}
/>
CPU {cpuPercent}%
</div>
</Link>
)}
{Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => {
if (name == "error-gpu") {
return;
}
let gpuTitle;
switch (name) {
case "amd-vaapi":
gpuTitle = "AMD GPU";
break;
case "intel-gpu":
gpuTitle = "Intel GPU";
break;
case "rockchip":
gpuTitle = "Rockchip GPU";
break;
default:
gpuTitle = name;
break;
}
const gpu = parseInt(stats.gpu);
if (isNaN(gpu)) {
return;
}
return (
<Link key={name} to="/system#general">
{" "}
<div
key={name}
className="flex cursor-pointer items-center gap-2 text-sm hover:underline"
>
<MdCircle
className={`size-2 ${
gpu < 50
? "text-success"
: gpu < 80
? "text-orange-400"
: "text-danger"
}`}
/>
{gpuTitle} {gpu}%
</div>
</Link>
);
})}
{activeProfile &&
(isAdmin ? (
<Link to="/settings?page=profiles">
<div className="flex cursor-pointer items-center gap-2 text-sm hover:underline">
<span
className={cn(
"size-2 shrink-0 rounded-full",
activeProfile.color.dot,
)}
/>
<span className="max-w-[150px] truncate">
{activeProfile.friendlyName}
</span>
</div>
</Link>
) : (
<div className="flex items-center gap-2 text-sm">
<span
className={cn(
"size-2 shrink-0 rounded-full",
activeProfile.color.dot,
)}
/>
<span className="max-w-[150px] truncate">
{activeProfile.friendlyName}
</span>
</div>
))}
</div>
<div className="no-scrollbar flex h-full max-w-[50%] items-center gap-2 overflow-x-auto">
{Object.entries(messages).length === 0 ? (
<div className="flex items-center gap-2 text-sm">
<FaCheck className="size-3 text-green-500" />
{t("stats.healthy")}
</div>
) : (
Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="flex h-full items-center gap-2">
{messageArray.map(({ text, color, link }: StatusMessage) => {
const message = (
<div
key={text}
className={`flex items-center gap-2 whitespace-nowrap text-sm ${link ? "cursor-pointer hover:underline" : ""}`}
>
<IoIosWarning
className={`size-5 ${color || "text-danger"}`}
/>
{text}
</div>
);
if (link) {
return (
<Link key={text} to={link}>
{message}
</Link>
);
} else {
return message;
}
})}
</div>
))
)}
</div>
</div>
);
}