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
227 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|