Merge branch 'blakeblackshear:dev' into dev

This commit is contained in:
Dmytro Marchuk
2026-05-17 08:50:35 +03:00
committed by GitHub
745 changed files with 32109 additions and 4377 deletions
@@ -0,0 +1,54 @@
/**
* Debug replay status factory.
*
* The Replay page polls /api/debug_replay/status every 1s via SWR.
* The no-session state shows an empty state; the active state
* renders the live camera image + debug toggles + objects/messages
* tabs. Used by replay.spec.ts.
*/
export type DebugReplayStatus = {
active: boolean;
replay_camera: string | null;
source_camera: string | null;
start_time: number | null;
end_time: number | null;
live_ready: boolean;
};
export function noSessionStatus(): DebugReplayStatus {
return {
active: false,
replay_camera: null,
source_camera: null,
start_time: null,
end_time: null,
live_ready: false,
};
}
export function activeSessionStatus(
opts: {
camera?: string;
sourceCamera?: string;
startTime?: number;
endTime?: number;
liveReady?: boolean;
} = {},
): DebugReplayStatus {
const {
camera = "front_door",
sourceCamera = "front_door",
startTime = Date.now() / 1000 - 3600,
endTime = Date.now() / 1000 - 1800,
liveReady = true,
} = opts;
return {
active: true,
replay_camera: camera,
source_camera: sourceCamera,
start_time: startTime,
end_time: endTime,
live_ready: liveReady,
};
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Face library factories.
*
* The /api/faces endpoint returns a record keyed by collection name
* with the list of face image filenames. Grouped training attempts
* live under the "train" key with filenames of the form
* `${event_id}-${timestamp}-${label}-${score}.webp`.
*
* Used by face-library.spec.ts and chat.spec.ts (attachment chip).
*/
export type FacesMock = Record<string, string[]>;
export function basicFacesMock(): FacesMock {
return {
alice: ["alice-1.webp", "alice-2.webp"],
bob: ["bob-1.webp"],
charlie: ["charlie-1.webp"],
};
}
export function emptyFacesMock(): FacesMock {
return {};
}
/**
* Adds a grouped recent-recognition training attempt to an existing
* faces mock. The grouping key on the backend is the event id — so
* images with the same event-id prefix render as one dialog-able card.
*/
export function withGroupedTrainingAttempt(
base: FacesMock,
opts: {
eventId: string;
attempts: Array<{ timestamp: number; label: string; score: number }>;
},
): FacesMock {
const trainImages = opts.attempts.map(
(a) => `${opts.eventId}-${a.timestamp}-${a.label}-${a.score}.webp`,
);
return {
...base,
train: [...(base.train ?? []), ...trainImages],
};
}
+4 -3
View File
@@ -113,11 +113,12 @@ export class ApiMocker {
route.fulfill({ json: [] }),
);
// Sub-labels and attributes (for explore filters)
await this.page.route("**/api/sub_labels", (route) =>
// Sub-labels and attributes (for explore filters).
// Use trailing ** so query-string variants (e.g. ?split_joined=1) match.
await this.page.route("**/api/sub_labels**", (route) =>
route.fulfill({ json: [] }),
);
await this.page.route("**/api/labels", (route) =>
await this.page.route("**/api/labels**", (route) =>
route.fulfill({ json: ["person", "car"] }),
);
await this.page.route("**/api/*/attributes", (route) =>
+25
View File
@@ -0,0 +1,25 @@
/**
* Clipboard read helper for e2e tests.
*
* Clipboard API requires a browser permission in headless mode.
* grantClipboardPermissions() must be called before any readClipboard()
* attempt. Used by logs.spec.ts (Copy button) and config-editor.spec.ts
* (Copy button).
*/
import type { BrowserContext, Page } from "@playwright/test";
/**
* Grant clipboard-read + clipboard-write permissions on the context.
* Call in beforeEach or at the top of a test before the Copy action.
*/
export async function grantClipboardPermissions(
context: BrowserContext,
): Promise<void> {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
}
/** Read the current clipboard contents via the page's navigator.clipboard. */
export async function readClipboard(page: Page): Promise<string> {
return page.evaluate(async () => await navigator.clipboard.readText());
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Monaco editor DOM helpers for e2e tests.
*
* Monaco is imported as a module-local object in the app and is NOT
* exposed on window; we drive + read through the rendered DOM and
* keyboard instead. Used by config-editor.spec.ts only.
*/
import { expect, type Page } from "@playwright/test";
/**
* Returns the current visible text of the first Monaco editor on the
* page. Monaco virtualizes long files — this reads only the rendered
* lines. For short configs (our mocks) that's the full content.
*/
export async function getMonacoVisibleText(page: Page): Promise<string> {
return page.locator(".monaco-editor .view-lines").first().innerText();
}
/**
* Focus the editor and replace its full content with `value` via
* keyboard. Uses Ctrl+A (Cmd+A on macOS Playwright is equivalent)
* + Delete + type. Works cross-platform because Playwright normalizes.
*/
export async function replaceMonacoValue(
page: Page,
value: string,
): Promise<void> {
const editor = page.locator(".monaco-editor").first();
await editor.click();
await page.keyboard.press("ControlOrMeta+A");
await page.keyboard.press("Delete");
// Use `type` with zero delay — Monaco handles each key.
await page.keyboard.type(value, { delay: 0 });
}
/**
* Returns true when the editor shows at least one error-severity
* marker. Monaco renders error underlines as `.squiggly-error` in
* the `.view-overlays` layer.
*/
export async function hasErrorMarkers(page: Page): Promise<boolean> {
const count = await page.locator(".monaco-editor .squiggly-error").count();
return count > 0;
}
/**
* Poll until an error marker appears. Monaco schedules marker updates
* asynchronously after content changes (debounce + schema validation).
*/
export async function waitForErrorMarker(
page: Page,
timeoutMs: number = 10_000,
): Promise<void> {
await expect
.poll(() => hasErrorMarkers(page), { timeout: timeoutMs })
.toBe(true);
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Overlay interaction helpers for Radix-based UI tests.
*
* These helpers exist to guard the class of bugs fixed by de-duping
* `@radix-ui/react-dismissable-layer` across the tree: body pointer-events
* getting stuck, dropdown typeahead breaking, tooltips re-popping after a
* dropdown closes, and related nested-overlay regressions.
*/
import { expect, type Page } from "@playwright/test";
/**
* Assert that `<body>` is interactive (no stuck `pointer-events: none`).
*
* Call after closing any overlay. This is the fast secondary assertion —
* test specs should also assert a user-visible behavior like "a button
* responded to a click" so the test fails on meaningful breakage rather
* than just a CSS invariant.
*/
export async function expectBodyInteractive(page: Page) {
const stuck = await page.evaluate(
() => document.body.style.pointerEvents === "none",
);
expect(stuck, "body.style.pointer-events stuck after overlay close").toBe(
false,
);
}
/**
* Wait until the `<body>` is no longer marked with `pointer-events: none`.
*
* Useful right after closing an overlay when Radix's cleanup runs in the
* next frame. Throws if the style does not clear within `timeoutMs`.
*/
export async function waitForBodyInteractive(page: Page, timeoutMs = 2000) {
await page.waitForFunction(
() => document.body.style.pointerEvents !== "none",
null,
{ timeout: timeoutMs },
);
}
+65
View File
@@ -0,0 +1,65 @@
/**
* WebSocket frame capture helper.
*
* The ws-mocker intercepts the /ws route, so Playwright's page-level
* `websocket` event never fires. This helper patches client-side
* WebSocket.prototype.send before any app code runs and mirrors every
* sent frame into a window-level array the test can read back.
*
* Used by live.spec.ts (feature toggles, PTZ preset commands) and
* config-editor.spec.ts (restart command via useRestart).
*/
import { expect, type Page } from "@playwright/test";
export type CapturedFrame = string;
declare global {
interface Window {
__sentWsFrames: CapturedFrame[];
}
}
/**
* Patch WebSocket.prototype.send to capture every outbound frame into
* window.__sentWsFrames. Must be called BEFORE page.goto().
*/
export async function installWsFrameCapture(page: Page): Promise<void> {
await page.addInitScript(() => {
window.__sentWsFrames = [];
const origSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (data) {
try {
window.__sentWsFrames.push(
typeof data === "string" ? data : "(binary)",
);
} catch {
// ignore — best-effort tracing
}
return origSend.call(this, data);
};
});
}
/** Read all captured frames at call time. */
export async function readWsFrames(page: Page): Promise<CapturedFrame[]> {
return page.evaluate(() => window.__sentWsFrames ?? []);
}
/**
* Poll until at least one captured frame matches the predicate.
* Throws via expect if the frame never arrives within timeout.
*/
export async function waitForWsFrame(
page: Page,
matcher: (frame: CapturedFrame) => boolean,
opts: { timeout?: number; message?: string } = {},
): Promise<void> {
const { timeout = 2_000, message } = opts;
await expect
.poll(async () => (await readWsFrames(page)).some(matcher), {
timeout,
message,
})
.toBe(true);
}
+14 -1
View File
@@ -79,7 +79,20 @@ export class WsMocker {
this.send("model_state", JSON.stringify({}));
}
if (data.topic === "embeddingsReindexProgress") {
this.send("embeddings_reindex_progress", JSON.stringify(null));
// Send a completed reindex state so Explore renders when
// semantic_search.enabled is true. A null payload leaves the page
// in a permanent loading spinner because !reindexState is truthy.
this.send(
"embeddings_reindex_progress",
JSON.stringify({
status: "completed",
processed_objects: 0,
total_objects: 0,
thumbnails: 0,
descriptions: 0,
time_remaining: null,
}),
);
}
if (data.topic === "birdseyeLayout") {
this.send("birdseye_layout", JSON.stringify(null));
+55
View File
@@ -0,0 +1,55 @@
/**
* Live dashboard + single-camera page object.
*
* Encapsulates selectors and viewport-conditional openers for the
* Live route. Does NOT own assertions — specs call expect on the
* locators returned from these getters.
*/
import type { Locator, Page } from "@playwright/test";
import { BasePage } from "./base.page";
export class LivePage extends BasePage {
constructor(page: Page, isDesktop: boolean) {
super(page, isDesktop);
}
/** The camera card wrapper on the dashboard, keyed by camera name. */
cameraCard(name: string): Locator {
return this.page.locator(`[data-camera='${name}']`);
}
/** Back button on the single-camera view header (desktop text). */
get backButton(): Locator {
return this.page.getByText("Back", { exact: true });
}
/** History button on the single-camera view header (desktop text). */
get historyButton(): Locator {
return this.page.getByText("History", { exact: true });
}
/** All CameraFeatureToggle elements (active + inactive). */
get featureToggles(): Locator {
// Use div selector to exclude NavItem anchor elements that share the same classes.
return this.page.locator(
"div.flex.flex-col.items-center.justify-center.bg-selected, div.flex.flex-col.items-center.justify-center.bg-secondary",
);
}
/** Only the active (bg-selected) feature toggles. */
get activeFeatureToggles(): Locator {
// Use div selector to exclude NavItem anchor elements that share the same classes.
return this.page.locator(
"div.flex.flex-col.items-center.justify-center.bg-selected",
);
}
/** Open the right-click context menu on a camera card (desktop only). */
async openContextMenuOn(cameraName: string): Promise<Locator> {
await this.cameraCard(cameraName).first().click({ button: "right" });
return this.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Review/events page object.
*
* Encapsulates severity tab, filter bar, calendar, and mobile filter
* drawer selectors. Does NOT own assertions.
*/
import type { Locator, Page } from "@playwright/test";
import { BasePage } from "./base.page";
export class ReviewPage extends BasePage {
constructor(page: Page, isDesktop: boolean) {
super(page, isDesktop);
}
get alertsTab(): Locator {
return this.page.getByLabel("Alerts");
}
get detectionsTab(): Locator {
return this.page.getByLabel("Detections");
}
get motionTab(): Locator {
return this.page.getByRole("radio", { name: "Motion" });
}
get camerasFilterTrigger(): Locator {
return this.page.getByRole("button", { name: /cameras/i }).first();
}
get calendarTrigger(): Locator {
return this.page.getByRole("button", { name: /24 hours|calendar|date/i });
}
get showReviewedToggle(): Locator {
return this.page.getByRole("button", { name: /reviewed/i });
}
get reviewItems(): Locator {
return this.page.locator(".review-item");
}
/** The filter popover content (desktop) or drawer (mobile). */
get filterOverlay(): Locator {
return this.page
.locator(
'[data-radix-popper-content-wrapper], [role="dialog"], [data-vaul-drawer]',
)
.first();
}
}
+2 -28
View File
@@ -14,10 +14,6 @@
*
* @mobile rule: every .spec.ts under specs/ (not specs/_meta/) must
* contain at least one test title or describe with the substring "@mobile".
*
* Specs in PENDING_REWRITE are exempt from all rules until they are
* rewritten with proper assertions and mobile coverage. Remove each
* entry when its spec is updated.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
@@ -28,24 +24,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const SPECS_DIR = resolve(__dirname, "..", "specs");
const META_PREFIX = resolve(SPECS_DIR, "_meta");
// Specs exempt from lint rules until they are rewritten with proper
// assertions and mobile coverage. Remove each entry when its spec is updated.
const PENDING_REWRITE = new Set([
"auth.spec.ts",
"chat.spec.ts",
"classification.spec.ts",
"config-editor.spec.ts",
"explore.spec.ts",
"export.spec.ts",
"face-library.spec.ts",
"live.spec.ts",
"logs.spec.ts",
"navigation.spec.ts",
"replay.spec.ts",
"review.spec.ts",
"system.spec.ts",
]);
const BANNED_PATTERNS = [
{
name: "page.waitForTimeout",
@@ -62,14 +40,12 @@ const BANNED_PATTERNS = [
{
name: "conditional count() assertion",
regex: /\bif\s*\(\s*\(?\s*await\s+[^)]*\.count\s*\(\s*\)\s*\)?\s*[><=!]/,
advice:
"Assertions must be unconditional. Use expect(...).toHaveCount(n).",
advice: "Assertions must be unconditional. Use expect(...).toHaveCount(n).",
},
{
name: "vacuous textContent length assertion",
regex: /expect\([^)]*\.length\)\.toBeGreaterThan\(0\)/,
advice:
"Assert specific content, not that some text exists.",
advice: "Assert specific content, not that some text exists.",
},
];
@@ -89,8 +65,6 @@ function walk(dir) {
}
function lintFile(file) {
const basename = file.split("/").pop();
if (PENDING_REWRITE.has(basename)) return [];
if (file.includes("/specs/settings/")) return [];
const errors = [];
+64 -101
View File
@@ -1,147 +1,110 @@
/**
* Auth and cross-cutting tests -- HIGH tier.
* Auth and role tests -- HIGH tier.
*
* Tests protected route access for admin/viewer roles,
* access denied page rendering, viewer nav restrictions,
* and all routes smoke test.
* Admin access to /system, /config, /logs; viewer access denied
* markers (via i18n heading, not a data-testid we don't own);
* viewer nav restrictions; all-routes smoke.
*/
import { test, expect } from "../fixtures/frigate-test";
import { viewerProfile } from "../fixtures/mock-data/profile";
test.describe("Auth - Admin Access @high", () => {
test("admin can access /system and sees system tabs", async ({
frigateApp,
}) => {
test.describe("Auth — admin access @high", () => {
test("admin /system renders general tab", async ({ frigateApp }) => {
await frigateApp.goto("/system");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await frigateApp.page.waitForTimeout(3000);
// System page should have named tab buttons
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 5_000,
timeout: 15_000,
});
});
test("admin can access /config and Monaco editor loads", async ({
frigateApp,
}) => {
test("admin /config renders Monaco editor", async ({ frigateApp }) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
const editor = frigateApp.page.locator(
".monaco-editor, [data-keybinding-context]",
);
await expect(editor.first()).toBeVisible({ timeout: 10_000 });
await expect(
frigateApp.page
.locator(".monaco-editor, [data-keybinding-context]")
.first(),
).toBeVisible({ timeout: 15_000 });
});
test("admin can access /logs and sees service tabs", async ({
frigateApp,
}) => {
test("admin /logs renders frigate tab", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
test("admin sees Classification nav on desktop", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(
frigateApp.page.locator('a[href="/classification"]'),
).toBeVisible();
});
});
test.describe("Auth - Viewer Restrictions @high", () => {
test("viewer sees Access Denied on /system", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/system");
await page.waitForTimeout(2000);
// Should show "Access Denied" text
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
test.describe("Auth — viewer restrictions @high", () => {
for (const path of ["/system", "/config", "/logs"]) {
test(`viewer on ${path} sees AccessDenied`, async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await frigateApp.page.goto(path);
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /access denied/i,
}),
).toBeVisible({ timeout: 10_000 });
});
}
test("viewer sees cameras on /", async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await frigateApp.page.goto("/");
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("viewer sees Access Denied on /config", async ({ frigateApp, page }) => {
test("viewer sees severity tabs on /review", async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/config");
await page.waitForTimeout(2000);
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
});
});
test("viewer sees Access Denied on /logs", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/logs");
await page.waitForTimeout(2000);
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
});
});
test("viewer can access Live page and sees cameras", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/");
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(page.locator("[data-camera='front_door']")).toBeVisible({
await frigateApp.page.goto("/review");
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
});
test("viewer can access Review page and sees severity tabs", async ({
test("viewer can access all non-admin routes without AccessDenied", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/review");
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(page.getByLabel("Alerts")).toBeVisible({ timeout: 5_000 });
});
test("viewer can access all main user routes without crash", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
const routes = ["/", "/review", "/explore", "/export", "/settings"];
for (const route of routes) {
await page.goto(route);
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await frigateApp.page.goto(route);
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /access denied/i,
}),
).toHaveCount(0);
}
});
});
test.describe("Auth - All Routes Smoke @high", () => {
test("all user routes render without crash", async ({ frigateApp }) => {
const routes = ["/", "/review", "/explore", "/export", "/settings"];
for (const route of routes) {
test.describe("Auth — viewer nav restrictions (desktop) @high", () => {
test.skip(({ frigateApp }) => frigateApp.isMobile, "Sidebar only on desktop");
test("viewer sidebar hides admin routes", async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await frigateApp.page.goto("/");
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
for (const href of ["/system", "/config", "/logs"]) {
await expect(
frigateApp.page.locator(`aside a[href='${href}']`),
).toHaveCount(0);
}
});
});
test.describe("Auth — all routes smoke @high @mobile", () => {
test("every common route renders #pageRoot", async ({ frigateApp }) => {
for (const route of ["/", "/review", "/explore", "/export", "/settings"]) {
await frigateApp.goto(route);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
}
});
test("admin routes render with specific content", async ({ frigateApp }) => {
// System page should have tab controls
await frigateApp.goto("/system");
await frigateApp.page.waitForTimeout(3000);
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 5_000,
});
// Logs page should have service tabs
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
});
+298 -21
View File
@@ -1,34 +1,311 @@
/**
* Chat page tests -- MEDIUM tier.
*
* Tests chat interface rendering, input area, and example prompt buttons.
* Starting state, NDJSON streaming contract (not SSE), assistant
* bubble grows as chunks arrive, error path, and mobile viewport.
*/
import { test, expect } from "../fixtures/frigate-test";
import { test, expect, type FrigateApp } from "../fixtures/frigate-test";
test.describe("Chat Page @medium", () => {
test("chat page renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
/**
* Install a window.fetch override on the page so that POSTs to
* chat/completion resolve with a real ReadableStream that emits the
* given chunks over time. This is the only way to validate
* chunk-by-chunk rendering through Playwright — page.route() does not
* support streaming responses.
*
* Must be called BEFORE frigateApp.goto(). The override also exposes
* `__chatRequests` on window so tests can assert the outgoing body.
*/
async function installChatStreamOverride(
app: FrigateApp,
chunks: Array<Record<string, unknown>>,
opts: { chunkDelayMs?: number; status?: number } = {},
) {
const { chunkDelayMs = 40, status = 200 } = opts;
await app.page.addInitScript(
({ chunks, chunkDelayMs, status }) => {
(window as unknown as { __chatRequests: unknown[] }).__chatRequests = [];
const origFetch = window.fetch;
window.fetch = async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: (input as Request).url;
if (url.includes("chat/completion")) {
const body =
init?.body instanceof String || typeof init?.body === "string"
? JSON.parse(init!.body as string)
: null;
(
window as unknown as { __chatRequests: unknown[] }
).__chatRequests.push({ url, body });
if (status !== 200) {
return new Response(JSON.stringify({ error: "boom" }), {
status,
});
}
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const chunk of chunks) {
await new Promise((r) => setTimeout(r, chunkDelayMs));
controller.enqueue(
encoder.encode(JSON.stringify(chunk) + "\n"),
);
}
controller.close();
},
});
return new Response(stream, { status: 200 });
}
return origFetch.call(window, input as RequestInfo, init);
};
},
{ chunks, chunkDelayMs, status },
);
}
test("chat page has interactive input or buttons", async ({ frigateApp }) => {
test.describe("Chat — starting state @medium", () => {
test("empty message list renders ChatStartingState with title and input", async ({
frigateApp,
}) => {
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
const interactive = frigateApp.page.locator("input, textarea, button");
const count = await interactive.count();
expect(count).toBeGreaterThan(0);
await expect(
frigateApp.page.getByRole("heading", { level: 1 }),
).toBeVisible({ timeout: 10_000 });
await expect(frigateApp.page.getByPlaceholder(/ask/i)).toBeVisible();
// Four quick-reply buttons from starting_requests.*
const quickReplies = frigateApp.page.locator(
"button:has-text('Show recent events'), button:has-text('Show camera status'), button:has-text('What happened'), button:has-text('Watch')",
);
await expect(quickReplies.first()).toBeVisible({ timeout: 5_000 });
});
});
test("chat input accepts text", async ({ frigateApp }) => {
test.describe("Chat — streaming @medium", () => {
test("submission POSTs to chat/completion with stream: true", async ({
frigateApp,
}) => {
await installChatStreamOverride(frigateApp, [
{ type: "content", delta: "Hel" },
{ type: "content", delta: "lo" },
]);
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
const input = frigateApp.page.locator("input, textarea").first();
if (await input.isVisible().catch(() => false)) {
await input.fill("What cameras detected a person today?");
const value = await input.inputValue();
expect(value.length).toBeGreaterThan(0);
}
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("hello chat");
await input.press("Enter");
await expect
.poll(
async () =>
frigateApp.page.evaluate(
() =>
(window as unknown as { __chatRequests: unknown[] })
.__chatRequests?.length ?? 0,
),
{ timeout: 5_000 },
)
.toBeGreaterThan(0);
const request = await frigateApp.page.evaluate(
() =>
(
window as unknown as {
__chatRequests: Array<{
url: string;
body: { stream: boolean; messages: Array<{ content: string }> };
}>;
}
).__chatRequests[0],
);
expect(request.body.stream).toBe(true);
expect(
request.body.messages[request.body.messages.length - 1].content,
).toBe("hello chat");
});
test("NDJSON content chunks accumulate in the assistant bubble", async ({
frigateApp,
}) => {
await installChatStreamOverride(
frigateApp,
[
{ type: "content", delta: "Hel" },
{ type: "content", delta: "lo, " },
{ type: "content", delta: "world!" },
],
{ chunkDelayMs: 50 },
);
await frigateApp.goto("/chat");
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("greet me");
await input.press("Enter");
await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({
timeout: 10_000,
});
});
test("tool_calls chunks render a ToolCallsGroup", async ({ frigateApp }) => {
await installChatStreamOverride(frigateApp, [
{
type: "tool_calls",
tool_calls: [
{
id: "call_1",
name: "search_objects",
arguments: { label: "person" },
},
],
},
{ type: "content", delta: "Searching for people." },
]);
await frigateApp.goto("/chat");
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("find people");
await input.press("Enter");
// ToolCallsGroup normalizes "search_objects" → "Search Objects" via
// normalizeName(). Match the rendered display label instead.
await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({
timeout: 10_000,
});
await expect(
frigateApp.page.getByText(/searching for people/i),
).toBeVisible({ timeout: 5_000 });
});
});
test.describe("Chat — stop @medium", () => {
test("Stop button aborts an in-flight stream and freezes the partial message", async ({
frigateApp,
}) => {
// A long chunk sequence with big delays gives us time to hit Stop.
await installChatStreamOverride(
frigateApp,
[
{ type: "content", delta: "First chunk. " },
{ type: "content", delta: "Second chunk. " },
{ type: "content", delta: "Third chunk. " },
],
{ chunkDelayMs: 300 },
);
await frigateApp.goto("/chat");
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("slow response please");
await input.press("Enter");
// Wait for the first chunk to render
await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({
timeout: 10_000,
});
// The Stop button is a destructive rounded button shown while isLoading.
// It contains only an FaStop SVG icon (no visible text). Find it by the
// destructive variant class or fall back to aria-label.
const stopBtn = frigateApp.page
.locator("button.bg-destructive, button[class*='destructive']")
.first();
await stopBtn.click({ timeout: 3_000 }).catch(async () => {
await frigateApp.page
.getByRole("button", { name: /stop|cancel/i })
.first()
.click();
});
// Third chunk should never appear.
await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0);
});
});
test.describe("Chat — error @medium", () => {
test("non-OK response renders an error banner", async ({ frigateApp }) => {
await installChatStreamOverride(frigateApp, [], { status: 500 });
await frigateApp.goto("/chat");
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("trigger error");
await input.press("Enter");
// The error banner is a role="alert" paragraph; target by role so we
// don't collide with the user-message bubble that contains "trigger
// error" (which would match /error/ in strict mode).
await expect(
frigateApp.page.getByRole("alert").filter({
hasText: /boom|something went wrong/i,
}),
).toBeVisible({ timeout: 5_000 });
});
});
test.describe("Chat — attachment chip @medium", () => {
test("attaching an event renders a ChatAttachmentChip", async ({
frigateApp,
}) => {
// The chat starts with an empty message list (ChatStartingState).
// After sending a message, ChatEntry with the paperclip button appears.
// We use the stream override so the first message completes quickly.
await installChatStreamOverride(frigateApp, [
{ type: "content", delta: "Done." },
]);
await frigateApp.goto("/chat");
// Send a first message to transition out of ChatStartingState so the
// full ChatEntry (with the paperclip) is visible.
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.fill("hello");
await input.press("Enter");
// Wait for the assistant response to complete so isLoading becomes false
// and the paperclip button is re-enabled.
await expect(frigateApp.page.getByText(/Done\./i)).toBeVisible({
timeout: 10_000,
});
// The paperclip button has aria-label from t("attachment_picker_placeholder")
// = "Attach an event".
const paperclip = frigateApp.page
.getByRole("button", { name: /attach an event/i })
.first();
await expect(paperclip).toBeVisible({ timeout: 5_000 });
await paperclip.click();
// The popover shows a paste input with placeholder "Or paste event ID".
const idInput = frigateApp.page
.locator('input[placeholder*="event" i], input[aria-label*="attach" i]')
.first();
await expect(idInput).toBeVisible({ timeout: 3_000 });
await idInput.fill("test-event-1");
await frigateApp.page
.getByRole("button", { name: /^attach$/i })
.first()
.click();
// The ChatAttachmentChip renders in the composer area. It shows an
// activity indicator while loading event data (event_ids API not mocked),
// so assert on the chip container being present in the composer.
await expect(
frigateApp.page.locator(
"[class*='inline-flex'][class*='rounded-lg'][class*='border']",
),
).toBeVisible({ timeout: 5_000 });
});
});
test.describe("Chat — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("chat input is focusable at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/chat");
const input = frigateApp.page.getByPlaceholder(/ask/i);
await expect(input).toBeVisible({ timeout: 10_000 });
await input.focus();
await expect(input).toBeFocused();
});
});
+209 -14
View File
@@ -1,33 +1,228 @@
/**
* Classification page tests -- MEDIUM tier.
*
* Tests model selection view rendering and interactive elements.
* Model list driven by config.classification.custom + per-model
* dataset fetches. Admin-only access.
*/
import { test, expect } from "../fixtures/frigate-test";
import { viewerProfile } from "../fixtures/mock-data/profile";
test.describe("Classification @medium", () => {
test("classification page renders without crash", async ({ frigateApp }) => {
const CUSTOM_MODELS = {
object_classifier: {
name: "object_classifier",
object_config: { objects: ["person"], classification_type: "sub_label" },
},
state_classifier: {
name: "state_classifier",
state_config: { cameras: { front_door: { crop: [0, 0, 1, 1] } } },
},
};
async function installDatasetRoute(
app: { page: import("@playwright/test").Page },
name: string,
body: Record<string, unknown> = { categories: {} },
) {
await app.page.route(
new RegExp(`/api/classification/${name}/dataset`),
(route) => route.fulfill({ json: body }),
);
}
async function installTrainRoute(
app: { page: import("@playwright/test").Page },
name: string,
) {
await app.page.route(
new RegExp(`/api/classification/${name}/train`),
(route) => route.fulfill({ json: [] }),
);
}
test.describe("Classification — model list @medium", () => {
test("custom models render by name", async ({ frigateApp }) => {
await frigateApp.installDefaults({
config: { classification: { custom: CUSTOM_MODELS } },
});
await installDatasetRoute(frigateApp, "object_classifier");
await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
timeout: 10_000,
});
});
test("classification page shows content and controls", async ({
frigateApp,
}) => {
test("empty custom map renders without crash", async ({ frigateApp }) => {
await frigateApp.installDefaults({
config: { classification: { custom: {} } },
});
await frigateApp.goto("/classification");
await frigateApp.page.waitForTimeout(2000);
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
});
test("classification page has interactive elements", async ({
test("toggling to states view switches the rendered card set", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({
config: { classification: { custom: CUSTOM_MODELS } },
});
await installDatasetRoute(frigateApp, "object_classifier");
await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("#pageRoot button");
const count = await buttons.count();
expect(count).toBeGreaterThanOrEqual(0);
// Objects is default — object_classifier visible, state_classifier hidden.
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
timeout: 10_000,
});
await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
// Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio".
const statesToggle = frigateApp.page
.getByRole("radio", { name: /state/i })
.first();
await expect(statesToggle).toBeVisible({ timeout: 5_000 });
await statesToggle.click();
await expect(frigateApp.page.getByText("state_classifier")).toBeVisible({
timeout: 5_000,
});
await expect(frigateApp.page.getByText("object_classifier")).toHaveCount(0);
});
});
test.describe("Classification — model detail navigation @medium", () => {
test("clicking a model card opens ModelTrainingView", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({
config: { classification: { custom: CUSTOM_MODELS } },
});
await installDatasetRoute(frigateApp, "object_classifier");
await installDatasetRoute(frigateApp, "state_classifier");
await installTrainRoute(frigateApp, "object_classifier");
await frigateApp.goto("/classification");
const objectCard = frigateApp.page.getByText("object_classifier").first();
await expect(objectCard).toBeVisible({ timeout: 10_000 });
await objectCard.click();
// ModelTrainingView renders a Back button (aria-label "Back").
// useOverlayState stores the selected model in window.history.state
// (not the URL), so we verify the state transition via the DOM.
await expect(
frigateApp.page.getByRole("button", { name: /back/i }),
).toBeVisible({ timeout: 5_000 });
// The model grid is no longer shown; state_classifier card is gone.
await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
});
});
test.describe("Classification — delete model (desktop) @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Delete action menu is desktop-focused",
);
test("deleting a model fires DELETE + PUT /config/set", async ({
frigateApp,
}) => {
let deleteCalled = false;
let configSetCalled = false;
// installDefaults must run first because Playwright matches routes in
// LIFO order — routes registered after installDefaults take precedence
// over the generic catch-all registered inside it.
await frigateApp.installDefaults({
config: { classification: { custom: CUSTOM_MODELS } },
});
await installDatasetRoute(frigateApp, "object_classifier");
await installDatasetRoute(frigateApp, "state_classifier");
// Register spy routes after installDefaults so they win over the catch-all.
await frigateApp.page.route(
/\/api\/classification\/object_classifier$/,
async (route) => {
if (route.request().method() === "DELETE") {
deleteCalled = true;
await route.fulfill({ json: { success: true } });
return;
}
return route.fallback();
},
);
await frigateApp.page.route("**/api/config/set", async (route) => {
if (route.request().method() === "PUT") configSetCalled = true;
await route.fulfill({ json: { success: true, require_restart: false } });
});
await frigateApp.goto("/classification");
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
timeout: 10_000,
});
// The card-level actions menu (FiMoreVertical three-dot icon) is a
// DropdownMenuTrigger with asChild on a BlurredIconButton div.
// Radix forwards aria-haspopup="menu" to the child element.
// Scope the selector to the model card grid to avoid hitting the
// settings sidebar trigger.
const cardGrid = frigateApp.page.locator(".grid.auto-rows-max");
await expect(cardGrid).toBeVisible({ timeout: 5_000 });
const trigger = cardGrid.locator('[aria-haspopup="menu"]').first();
await expect(trigger).toBeVisible({ timeout: 5_000 });
await trigger.click();
const deleteItem = frigateApp.page
.getByRole("menuitem", { name: /delete/i })
.first();
await expect(deleteItem).toBeVisible({ timeout: 5_000 });
await deleteItem.click();
// Confirm the AlertDialog.
const alert = frigateApp.page.getByRole("alertdialog");
await expect(alert).toBeVisible({ timeout: 5_000 });
await alert
.getByRole("button", { name: /delete|confirm/i })
.first()
.click();
await expect.poll(() => deleteCalled, { timeout: 5_000 }).toBe(true);
await expect.poll(() => configSetCalled, { timeout: 5_000 }).toBe(true);
});
});
test.describe("Classification — admin only @medium", () => {
test("viewer navigating to /classification is redirected to access-denied", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await frigateApp.page.goto("/classification");
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/\/unauthorized/, {
timeout: 10_000,
});
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /access denied/i,
}),
).toBeVisible({ timeout: 10_000 });
});
});
test.describe("Classification — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("page renders at mobile viewport", async ({ frigateApp }) => {
await frigateApp.installDefaults({
config: { classification: { custom: CUSTOM_MODELS } },
});
await installDatasetRoute(frigateApp, "object_classifier");
await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
});
});
+266 -34
View File
@@ -1,44 +1,276 @@
/**
* Config Editor page tests -- MEDIUM tier.
* Config Editor tests -- MEDIUM tier.
*
* Tests Monaco editor loading, YAML content rendering,
* save button presence, and copy button interaction.
* Monaco load + value, Save (config/save?save_option=saveonly),
* Save error path, Save and Restart (WS frame via useRestart),
* Copy (clipboard), schema markers.
*/
import { test, expect } from "../fixtures/frigate-test";
import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
import {
getMonacoVisibleText,
replaceMonacoValue,
waitForErrorMarker,
} from "../helpers/monaco";
test.describe("Config Editor @medium", () => {
test("config editor loads Monaco editor with content", async ({
frigateApp,
}) => {
const SAMPLE_CONFIG =
"mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n";
async function installSaveRoute(
app: { page: import("@playwright/test").Page },
status: number,
body: Record<string, unknown>,
): Promise<{
capturedUrl: () => string | null;
capturedBody: () => string | null;
}> {
let lastUrl: string | null = null;
let lastBody: string | null = null;
await app.page.route("**/api/config/save**", async (route) => {
lastUrl = route.request().url();
lastBody = route.request().postData();
await route.fulfill({ status, json: body });
});
return {
capturedUrl: () => lastUrl,
capturedBody: () => lastBody,
};
}
test.describe("Config Editor — Monaco @medium", () => {
test("editor loads with mocked configRaw content", async ({ frigateApp }) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
// Monaco editor should render with a specific class
const editor = frigateApp.page.locator(
".monaco-editor, [data-keybinding-context]",
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
// Assert via DOM-rendered visible text (Monaco virtualizes — works
// for short configs which covers our mocked content).
await expect
.poll(() => getMonacoVisibleText(frigateApp.page), { timeout: 10_000 })
.toContain("front_door");
});
});
test.describe("Config Editor — Save @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Save button copy is desktop-visible (hidden md:block)",
);
test("clicking Save Only POSTs config/save?save_option=saveonly", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
const capture = await installSaveRoute(frigateApp, 200, {
message: "Config saved",
});
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Save Only").click();
await expect
.poll(() => capture.capturedUrl(), { timeout: 5_000 })
.toMatch(/config\/save\?save_option=saveonly/);
// Body is the raw YAML as text/plain
await expect
.poll(() => capture.capturedBody(), { timeout: 5_000 })
.toContain("front_door");
});
test("Save error shows the server message in the error area", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await installSaveRoute(frigateApp, 400, {
message: "Invalid field `cameras.front_door`",
});
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Save Only").click();
await expect(frigateApp.page.getByText(/Invalid field/i)).toBeVisible({
timeout: 5_000,
});
});
});
test.describe("Config Editor — Save and Restart @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Save and Restart button copy is desktop-visible",
);
test("Save and Restart opens dialog; confirm sends WS restart frame", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await installSaveRoute(frigateApp, 200, { message: "Saved" });
await installWsFrameCapture(frigateApp.page);
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Save & Restart").click();
const dialog = frigateApp.page.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
await dialog.getByRole("button", { name: /restart/i }).click();
await waitForWsFrame(
frigateApp.page,
(frame) => frame.includes('"restart"') || frame.includes("restart"),
{ message: "useRestart should send a WS frame on the restart topic" },
);
});
test("cancelling the restart dialog leaves body interactive", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await installSaveRoute(frigateApp, 200, { message: "Saved" });
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Save & Restart").click();
const dialog = frigateApp.page.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
await dialog.getByRole("button", { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 3_000 });
await expect(
frigateApp.page.locator(".monaco-editor").first(),
).toBeVisible();
});
});
test.describe("Config Editor — Copy @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Copy button copy is desktop-visible",
);
test("Copy places the editor value in the clipboard", async ({
frigateApp,
context,
}) => {
await grantClipboardPermissions(context);
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Copy Config").click();
await expect
.poll(() => readClipboard(frigateApp.page), { timeout: 5_000 })
.toContain("front_door");
});
});
test.describe("Config Editor — schema markers @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Schema validation assumes focused desktop editing",
);
test("invalid YAML renders at least one error marker in the DOM", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
// Replace editor contents with clearly invalid YAML via keyboard.
await replaceMonacoValue(
frigateApp.page,
"this is not: [yaml: and has {unbalanced",
);
// Monaco debounces marker evaluation; the .squiggly-error decoration
// appears asynchronously in the .view-overlays layer.
await waitForErrorMarker(frigateApp.page);
});
});
test.describe("Config Editor — Cmd+S keyboard shortcut @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Keyboard save shortcut is desktop-only",
);
test("Cmd/Ctrl+S fires the same config/save POST as the Save button", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
const capture = await installSaveRoute(frigateApp, 200, {
message: "Saved",
});
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
// Focus the editor so Monaco's keybinding receives the shortcut.
await frigateApp.page.locator(".monaco-editor").first().click();
await frigateApp.page.keyboard.press("ControlOrMeta+s");
await expect
.poll(() => capture.capturedUrl(), { timeout: 5_000 })
.toMatch(/config\/save\?save_option=saveonly/);
});
});
test.describe("Config Editor — Safe Mode auto-validation @medium", () => {
test("safe-mode config auto-posts on mount and shows the inline error", async ({
frigateApp,
}) => {
// Thread safe_mode: true through the config override, then stub
// config/save to return a validation error. The page's
// initialValidationRef effect runs on mount and POSTs
// config/save?save_option=saveonly with the raw config; the 400
// surfaces through setError.
// installDefaults must come first so our specific route wins (LIFO).
await frigateApp.installDefaults({
config: { safe_mode: true } as unknown as Record<string, unknown>,
configRaw: "cameras:\n front_door:\n ffmpeg: {}\n",
});
let autoSaveCalled = false;
await frigateApp.page.route("**/api/config/save**", async (route) => {
autoSaveCalled = true;
await route.fulfill({
status: 400,
json: { message: "safe-mode validation failure" },
});
});
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true);
await expect(
frigateApp.page.getByText(/safe-mode validation failure/i),
).toBeVisible({ timeout: 5_000 });
});
});
test.describe("Config Editor — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("editor renders at narrow viewport", async ({ frigateApp }) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config");
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
{ timeout: 15_000 },
);
await expect(editor.first()).toBeVisible({ timeout: 10_000 });
});
test("config editor has action buttons", async ({ frigateApp }) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
const buttons = frigateApp.page.locator("button");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
test("config editor button clicks do not crash", async ({ frigateApp }) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
// Find buttons with SVG icons (copy, save, etc.)
const iconButtons = frigateApp.page.locator("button:has(svg)");
const count = await iconButtons.count();
if (count > 0) {
// Click the first icon button (likely copy)
await iconButtons.first().click();
await frigateApp.page.waitForTimeout(500);
}
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
+240 -72
View File
@@ -1,97 +1,265 @@
/**
* Explore page tests -- HIGH tier.
*
* Tests search input with text entry and clearing, camera filter popover
* opening with camera names, and content rendering with mock events.
* Search input, Enter submission, camera filter popover (desktop),
* event grid rendering with mocked events, mobile filter drawer.
*
* DEVIATION NOTES (from original plan):
*
* 1. Search input: InputWithTags is only rendered when
* config.semantic_search.enabled is true. Tests that exercise the search
* input override the config accordingly, using model:"genai" (not in the
* JINA_EMBEDDING_MODELS list) so the page skips local model-state checks
* and renders without waiting for model-download WS messages.
*
* 2. Filter buttons (Cameras, Labels, More Filters): SearchFilterGroup is
* only rendered when hasExistingSearch is true. Tests navigate with a URL
* param (?labels=person) to surface the filter bar.
*
* 3. Cameras button: accessible name is "Cameras Filter" (aria-label), not
* "All Cameras" (inner text). Use getByLabel("Cameras Filter").
*
* 4. Labels: button accessible name is "Labels" (aria-label). With
* ?labels=person, the text shows "Person" rather than "All Labels".
* Use getByLabel("Labels").
*
* 5. Sub-labels / Zones: These live inside the "More Filters" dialog
* (SearchFilterDialog), not as standalone top-level buttons. The Zones
* test opens "More Filters" and asserts zone content from config.
*
* 6. similarity_search_id URL param: This param does not exist in the app.
* The correct entrypoint for similarity search is
* ?search_type=similarity&event_id=<id>. The test uses this URL and
* polls for the resulting API request.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Explore Page - Search @high", () => {
test("explore page renders with filter buttons", async ({ frigateApp }) => {
// Semantic search config override used by multiple tests. Using model:
// "genai" (not in JINA_EMBEDDING_MODELS) sets isGenaiEmbeddings=true, which
// skips local model-state checks and lets the page render without waiting for
// individual model download WS messages. The WS mocker returns a completed
// reindexState so !reindexState is false and the loading gate clears.
const SEMANTIC_SEARCH_CONFIG = {
semantic_search: { enabled: true, model: "genai" },
} as const;
// ---------------------------------------------------------------------------
// Search input (semantic_search must be enabled)
// ---------------------------------------------------------------------------
test.describe("Explore — search @high", () => {
test("search input accepts text and clears", async ({ frigateApp }) => {
// Enable semantic search so InputWithTags renders.
await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
await frigateApp.goto("/explore");
const searchInput = frigateApp.page.locator("input").first();
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await searchInput.fill("person");
await expect(searchInput).toHaveValue("person");
await searchInput.fill("");
await expect(searchInput).toHaveValue("");
});
test("Enter submission does not crash the page", async ({ frigateApp }) => {
await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
await frigateApp.goto("/explore");
const searchInput = frigateApp.page.locator("input").first();
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await searchInput.fill("car in driveway");
await searchInput.press("Enter");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
const buttons = frigateApp.page.locator("#pageRoot button");
await expect(buttons.first()).toBeVisible({ timeout: 10_000 });
});
test("search input accepts text and can be cleared", async ({
frigateApp,
}) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const searchInput = frigateApp.page.locator("input").first();
if (await searchInput.isVisible()) {
await searchInput.fill("person");
await expect(searchInput).toHaveValue("person");
await searchInput.fill("");
await expect(searchInput).toHaveValue("");
}
});
test("search input submits on Enter", async ({ frigateApp }) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const searchInput = frigateApp.page.locator("input").first();
if (await searchInput.isVisible()) {
await searchInput.fill("car in driveway");
await searchInput.press("Enter");
await frigateApp.page.waitForTimeout(1000);
// Page should not crash after search submit
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}
});
});
test.describe("Explore Page - Filters @high", () => {
test("camera filter button opens popover with camera names (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i,
});
if (await camerasBtn.isVisible().catch(() => false)) {
await camerasBtn.click();
await frigateApp.page.waitForTimeout(500);
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Camera names from config should be in the popover
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
}
// ---------------------------------------------------------------------------
// Filter bar — desktop only
// Filter buttons appear once hasExistingSearch is true (URL params present).
// ---------------------------------------------------------------------------
test.describe("Explore — filters (desktop) @high", () => {
test.skip(({ frigateApp }) => frigateApp.isMobile, "Desktop popovers");
test("Cameras popover lists configured cameras", async ({ frigateApp }) => {
// Navigate with a labels filter param so the filter bar renders.
await frigateApp.goto("/explore?labels=person");
// CamerasFilterButton has aria-label="Cameras Filter". Use getByLabel to
// match against the accessible name (not the inner "All Cameras" text).
const camerasBtn = frigateApp.page.getByLabel("Cameras Filter").first();
await expect(camerasBtn).toBeVisible({ timeout: 10_000 });
await camerasBtn.click();
// DropdownMenu on desktop wraps content in data-radix-popper-content-wrapper.
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
});
test("filter button opens and closes overlay cleanly", async ({
test("Labels filter lists labels from config", async ({ frigateApp }) => {
// Navigate with an existing search so the filter bar renders.
await frigateApp.goto("/explore?labels=person");
// GeneralFilterButton has aria-label="Labels". With ?labels=person the
// button text shows "Person" (the selected label), but the aria-label
// remains "Labels".
const labelsBtn = frigateApp.page.getByLabel("Labels").first();
await expect(labelsBtn).toBeVisible({ timeout: 10_000 });
await labelsBtn.click();
// PlatformAwareDialog renders on desktop as a dropdown/popover overlay.
const overlay = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
);
await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
// "person" is already selected (it's in the URL); assert it appears in
// the overlay content.
await expect(overlay.first().getByText(/person/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
test("Sub-labels filter renders inside More Filters dialog", async ({
frigateApp,
}) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const firstButton = frigateApp.page.locator("#pageRoot button").first();
await expect(firstButton).toBeVisible({ timeout: 5_000 });
await firstButton.click();
await frigateApp.page.waitForTimeout(500);
// Sub-labels live inside SearchFilterDialog ("More Filters" button).
// With sub_labels mocked as [], the section still renders its heading.
await frigateApp.page.route("**/api/sub_labels**", (route) =>
route.fulfill({ json: [] }),
);
await frigateApp.goto("/explore?labels=person");
const moreBtn = frigateApp.page.getByLabel("More Filters").first();
await expect(moreBtn).toBeVisible({ timeout: 10_000 });
await moreBtn.click();
const overlay = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
);
await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
// "Sub Labels" section heading always renders inside the dialog.
await expect(
frigateApp.page.getByText(/sub.?label/i).first(),
).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
test("Zones filter lists configured zones inside More Filters dialog", async ({
frigateApp,
}) => {
// Override config to guarantee a known zone on front_door.
await frigateApp.installDefaults({
config: {
cameras: {
front_door: {
zones: {
front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" },
},
},
},
},
});
await frigateApp.goto("/explore?labels=person");
const moreBtn = frigateApp.page.getByLabel("More Filters").first();
await expect(moreBtn).toBeVisible({ timeout: 10_000 });
await moreBtn.click();
const overlay = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
);
await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
await expect(frigateApp.page.getByText(/front.?yard/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300);
// Page is still functional after open/close cycle
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
});
test.describe("Explore Page - Content @high", () => {
test("explore page shows content with mock events", async ({
// ---------------------------------------------------------------------------
// Content
// ---------------------------------------------------------------------------
test.describe("Explore — content @high", () => {
test("page renders with mock events", async ({ frigateApp }) => {
await frigateApp.goto("/explore");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
await expect(
frigateApp.page.locator("#pageRoot button").first(),
).toBeVisible({ timeout: 10_000 });
});
test("empty events renders without crash", async ({ frigateApp }) => {
await frigateApp.installDefaults({ events: [] });
await frigateApp.goto("/explore");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
});
test("search fires a /api/events request with the query", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
const eventsRequests: string[] = [];
frigateApp.page.on("request", (req) => {
const url = req.url();
if (/\/api\/events/.test(url)) eventsRequests.push(url);
});
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(3000);
const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText?.length).toBeGreaterThan(0);
const searchInput = frigateApp.page.locator("input").first();
await expect(searchInput).toBeVisible({ timeout: 10_000 });
const before = eventsRequests.length;
await searchInput.fill("person in driveway");
await searchInput.press("Enter");
await expect
.poll(() => eventsRequests.length > before, { timeout: 5_000 })
.toBe(true);
});
});
// ---------------------------------------------------------------------------
// Similarity search URL param
// ---------------------------------------------------------------------------
test.describe("Explore — similarity search (desktop) @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Similarity trigger is hover-based; desktop-focused",
);
test("URL similarity search params fetch events", async ({ frigateApp }) => {
const eventsRequests: string[] = [];
frigateApp.page.on("request", (req) => {
const url = req.url();
if (/\/api\/events/.test(url)) eventsRequests.push(url);
});
// The app uses search_type=similarity&event_id=<id> (not
// similarity_search_id). This exercises the same similarity search code
// path as clicking "Find Similar" on a thumbnail.
// Use a valid event-id format (timestamp.fractional-alphanumeric).
await frigateApp.goto(
"/explore?search_type=similarity&event_id=1712412000.000000-abc123",
);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
// Poll to allow any pending SWR fetch to complete and be captured.
await expect
.poll(() => eventsRequests.length, { timeout: 5_000 })
.toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// Mobile
// ---------------------------------------------------------------------------
test.describe("Explore — mobile @high @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("search input is focusable at mobile viewport", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
await frigateApp.goto("/explore");
const searchInput = frigateApp.page.locator("input").first();
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await searchInput.focus();
await expect(searchInput).toBeFocused();
});
});
+110
View File
@@ -1,4 +1,114 @@
import { test, expect } from "../fixtures/frigate-test";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
test.describe("Export Page - Delete race @high", () => {
// Empirical guard for radix-ui/primitives#3445: when a modal DropdownMenu
// opens an AlertDialog and the AlertDialog's confirm action causes the
// parent's optimistic cache update to unmount the card, we want to know
// whether the deduped react-dismissable-layer (1.1.11) handles the
// pointer-events stack cleanup or whether `modal={false}` is still
// required on the DropdownMenu. The classic "canonical" pattern, distinct
// from the FaceSelectionDialog auto-unmount race already covered by
// face-library.spec.ts.
test("deleting an export via dropdown→alert→confirm leaves body interactive", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
const initialExports = [
{
id: "export-race-001",
camera: "front_door",
name: "Race - Test Export",
date: 1775490731.3863528,
video_path: "/exports/export-race-001.mp4",
thumb_path: "/exports/export-race-001-thumb.jpg",
in_progress: false,
export_case_id: null,
},
];
let deleted = false;
await frigateApp.installDefaults({
exports: initialExports,
});
// Flip /api/export to empty after the delete POST is observed so the
// page's SWR mutate sees the export gone.
await frigateApp.page.route("**/api/export**", async (route) => {
const payload = deleted ? [] : initialExports;
await route.fulfill({ json: payload });
});
await frigateApp.page.route("**/api/exports/delete", async (route) => {
deleted = true;
const delayMs = Number(
(globalThis as { process?: { env?: Record<string, string> } }).process
?.env?.DELETE_DELAY_MS ?? "100",
);
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
await route.fulfill({ json: { success: true } });
});
await frigateApp.goto("/export");
await expect(frigateApp.page.getByText("Race - Test Export")).toBeVisible({
timeout: 5_000,
});
// Open the kebab menu on the export card. The kebab uses the
// (misleading) aria-label "Edit name" from ExportCard's source — it
// wraps the FiMoreVertical icon. There is exactly one such button on
// the page once we have a single export rendered.
const kebab = frigateApp.page
.getByRole("button", { name: /edit name/i })
.first();
await expect(kebab).toBeVisible({ timeout: 5_000 });
await kebab.click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 3_000 });
// Delete Export
await menu
.getByRole("menuitem", { name: /delete export/i })
.first()
.click();
// AlertDialog at page level. The confirm button's accessible name is
// "Delete Export" (its aria-label), the visible text is just "Delete".
const confirm = frigateApp.page.getByRole("alertdialog");
await expect(confirm).toBeVisible({ timeout: 3_000 });
await confirm
.getByRole("button", { name: /^delete export$/i })
.first()
.click();
// The card optimistically disappears, the dialog closes, and body
// pointer-events must come unstuck.
await expect(
frigateApp.page.getByText("Race - Test Export"),
).not.toBeVisible({ timeout: 5_000 });
await waitForBodyInteractive(frigateApp.page, 5_000);
await expectBodyInteractive(frigateApp.page);
// Sanity: another page-level button still responds.
const newCase = frigateApp.page.getByRole("button", { name: /new case/i });
await expect(newCase).toBeVisible({ timeout: 3_000 });
await newCase.click();
await expect(
frigateApp.page.getByRole("dialog").filter({ hasText: /create case/i }),
).toBeVisible({ timeout: 3_000 });
});
});
test.describe("Export Page - Overview @high", () => {
test("renders uncategorized exports and case cards from mock data", async ({
+527 -17
View File
@@ -1,32 +1,542 @@
/**
* Face Library page tests -- MEDIUM tier.
* Face Library page tests -- HIGH tier.
*
* Tests face grid rendering, empty state, and interactive controls.
* Collection selector, face tiles, grouped recent-recognition dialog
* (migrated from radix-overlay-regressions.spec.ts), and mobile
* library selector.
*/
import { test, expect } from "../fixtures/frigate-test";
import { type Locator } from "@playwright/test";
import { test, expect, type FrigateApp } from "../fixtures/frigate-test";
import {
basicFacesMock,
emptyFacesMock,
withGroupedTrainingAttempt,
} from "../fixtures/mock-data/faces";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
test.describe("Face Library @medium", () => {
test("face library page renders without crash", async ({ frigateApp }) => {
const GROUPED_EVENT_ID = "1775487131.3863528-abc123";
function groupedFacesMock() {
return withGroupedTrainingAttempt(basicFacesMock(), {
eventId: GROUPED_EVENT_ID,
attempts: [
{ timestamp: 1775487131.3863528, label: "unknown", score: 0.95 },
{ timestamp: 1775487132.3863528, label: "unknown", score: 0.91 },
],
});
}
async function installGroupedFaces(app: FrigateApp) {
await app.api.install({
events: [
{
id: GROUPED_EVENT_ID,
label: "person",
sub_label: null,
camera: "front_door",
start_time: 1775487131.3863528,
end_time: 1775487161.3863528,
false_positive: false,
zones: ["front_yard"],
thumbnail: null,
has_clip: true,
has_snapshot: true,
retain_indefinitely: false,
plus_id: null,
model_hash: "abc123",
detector_type: "cpu",
model_type: "ssd",
data: {
top_score: 0.92,
score: 0.92,
region: [0.1, 0.1, 0.5, 0.8],
box: [0.2, 0.15, 0.45, 0.75],
area: 0.18,
ratio: 0.6,
type: "object",
path_data: [],
},
},
],
faces: groupedFacesMock(),
});
}
async function openGroupedFaceDialog(app: FrigateApp): Promise<Locator> {
await installGroupedFaces(app);
await app.goto("/faces");
const groupedImage = app.page
.locator('img[src*="clips/faces/train/"]')
.first();
const groupedCard = groupedImage.locator("xpath=..");
await expect(groupedImage).toBeVisible({ timeout: 5_000 });
await groupedCard.click();
const dialog = app.page
.getByRole("dialog")
.filter({ has: app.page.locator('img[src*="clips/faces/train/"]') })
.first();
await expect(dialog).toBeVisible({ timeout: 5_000 });
await expect(dialog.locator('img[src*="clips/faces/train/"]')).toHaveCount(2);
return dialog;
}
/**
* Opens the LibrarySelector dropdown (the single button at the top-left of
* the Face Library page) and returns the dropdown menu locator.
*
* The LibrarySelector is a single DropdownMenu whose trigger shows the
* current tab name + count (e.g. "Recent Recognitions (0)"). Named face
* collections (alice, bob, charlie) are items inside this dropdown.
*/
async function openLibraryDropdown(app: FrigateApp): Promise<Locator> {
// The trigger is the first button on the page with a parenthesised count.
const trigger = app.page
.getByRole("button")
.filter({ hasText: /\(\d+\)/ })
.first();
await expect(trigger).toBeVisible({ timeout: 10_000 });
await trigger.click();
const menu = app.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 5_000 });
return menu;
}
test.describe("Face Library — collection selector @high", () => {
test("selector shows named face collections", async ({ frigateApp }) => {
await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.goto("/faces");
// Named collections appear in the LibrarySelector dropdown.
const menu = await openLibraryDropdown(frigateApp);
await expect(menu.getByText(/alice/i).first()).toBeVisible({
timeout: 5_000,
});
});
test("empty state renders when no faces exist", async ({ frigateApp }) => {
await frigateApp.installDefaults({ faces: emptyFacesMock() });
await frigateApp.goto("/faces");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(
frigateApp.page.locator('img[src*="/clips/faces/"]'),
).toHaveCount(0);
});
test("face library shows empty state with no faces", async ({
frigateApp,
}) => {
test("tiles render for each named collection", async ({ frigateApp }) => {
await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.goto("/faces");
await frigateApp.page.waitForTimeout(2000);
// With empty faces mock, should show empty state or content
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
// Open the dropdown — collections list shows "alice (2)" and "bob (1)".
const menu = await openLibraryDropdown(frigateApp);
await expect(
menu.locator('[role="menuitem"]').filter({ hasText: /alice/i }).first(),
).toBeVisible({ timeout: 5_000 });
await expect(
menu.locator('[role="menuitem"]').filter({ hasText: /bob/i }).first(),
).toBeVisible();
});
});
test("face library has interactive buttons", async ({ frigateApp }) => {
test.describe("Face Library — delete flow (desktop) @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Delete action menu is desktop-focused",
);
test("deleting a collection fires POST /faces/<name>/delete", async ({
frigateApp,
}) => {
let deleteUrl: string | null = null;
let deleteBody: unknown = null;
// Install base mocks first, then register our more-specific route AFTER
// so it takes priority over the ApiMocker catch-all (Playwright LIFO order).
await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.page.route(
/\/api\/faces\/[^/]+\/delete/,
async (route) => {
deleteUrl = route.request().url();
deleteBody = route.request().postDataJSON();
await route.fulfill({ json: { success: true } });
},
);
await frigateApp.goto("/faces");
// Open the LibrarySelector dropdown and click the trash icon next
// to the alice row. The trash icon is a ghost-variant Button inside
// the DropdownMenuItem — it becomes visible on hover/focus.
const menu = await openLibraryDropdown(frigateApp);
const aliceRow = menu
.locator('[role="menuitem"]')
.filter({ hasText: /alice/i })
.first();
await expect(aliceRow).toBeVisible({ timeout: 5_000 });
// Hover first to make hover-only opacity-0 buttons visible.
await aliceRow.hover();
// The icon buttons have no aria-label or title. The row renders exactly
// two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2
// (delete). This order is determined by FaceLibrary.tsx and is stable.
const trashBtn = aliceRow.locator("button").nth(1);
await trashBtn.click();
// The delete confirmation is a Dialog (not AlertDialog) in this flow.
const dialog = frigateApp.page.getByRole("dialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
await dialog
.getByRole("button", { name: /delete/i })
.first()
.click();
await expect
.poll(() => deleteUrl, { timeout: 5_000 })
.toMatch(/\/faces\/alice\/delete/);
expect(deleteBody).toMatchObject({ ids: expect.any(Array) });
});
});
test.describe("Face Library — rename flow (desktop) @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Rename action menu is desktop-focused",
);
test("renaming a collection fires PUT /faces/<name>/rename", async ({
frigateApp,
}) => {
let renameUrl: string | null = null;
let renameBody: unknown = null;
// Install base mocks first, then register our more-specific route AFTER
// so it takes priority over the ApiMocker catch-all (Playwright LIFO order).
await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.page.route(
/\/api\/faces\/[^/]+\/rename/,
async (route) => {
renameUrl = route.request().url();
renameBody = route.request().postDataJSON();
await route.fulfill({ json: { success: true } });
},
);
await frigateApp.goto("/faces");
// Open the LibrarySelector dropdown and click the pencil (rename) icon
// next to alice. The icon is a ghost Button inside the DropdownMenuItem.
const menu = await openLibraryDropdown(frigateApp);
const aliceRow = menu
.locator('[role="menuitem"]')
.filter({ hasText: /alice/i })
.first();
await expect(aliceRow).toBeVisible({ timeout: 5_000 });
await aliceRow.hover();
// The icon buttons have no aria-label or title. The row renders exactly
// two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2
// (delete). This order is determined by FaceLibrary.tsx and is stable.
const pencilBtn = aliceRow.locator("button").nth(0);
await pencilBtn.click();
// TextEntryDialog — fill the input and confirm.
const dialog = frigateApp.page.getByRole("dialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
await dialog.locator("input").first().fill("alice_renamed");
await dialog
.getByRole("button", { name: /save|rename|confirm/i })
.first()
.click();
await expect
.poll(() => renameUrl, { timeout: 5_000 })
.toMatch(/\/faces\/alice\/rename/);
expect(renameBody).toEqual({ new_name: "alice_renamed" });
});
});
test.describe("Face Library — upload flow @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Upload button has no accessible text on mobile — icon-only on narrow viewports",
);
test("Upload button opens the upload dialog", async ({ frigateApp }) => {
await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.goto("/faces");
// Navigate to the alice tab by opening the dropdown and clicking alice.
const menu = await openLibraryDropdown(frigateApp);
await menu
.locator('[role="menuitem"]')
.filter({ hasText: /alice/i })
.first()
.click();
// After switching to alice, the Upload Image button appears in the toolbar.
const uploadBtn = frigateApp.page
.getByRole("button")
.filter({ hasText: /upload/i })
.first();
await expect(uploadBtn).toBeVisible({ timeout: 5_000 });
await uploadBtn.click();
// UploadImageDialog renders a file input + confirm button.
const dialog = frigateApp.page.getByRole("dialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
await expect(dialog.locator('input[type="file"]')).toHaveCount(1);
});
});
test.describe("FaceSelectionDialog @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Grouped dropdown flow is desktop-only",
);
test("reclassify dropdown selects a name and closes cleanly", async ({
frigateApp,
}) => {
// Migrated from radix-overlay-regressions.spec.ts.
const dialog = await openGroupedFaceDialog(frigateApp);
const triggers = dialog.locator('[aria-haspopup="menu"]');
await expect(triggers).toHaveCount(2);
await triggers.first().click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 5_000 });
await menu.getByRole("menuitem", { name: /^bob$/i }).click();
await expect(menu).not.toBeVisible({ timeout: 3_000 });
await expect(dialog).toBeVisible();
const tooltipVisible = await frigateApp.page
.locator('[role="tooltip"]')
.filter({ hasText: /train face/i })
.isVisible()
.catch(() => false);
expect(
tooltipVisible,
"Train Face tooltip popped after dropdown closed — focus-restore regression",
).toBe(false);
});
test("second dropdown open accepts typeahead keyboard input", async ({
frigateApp,
}) => {
// Migrated from radix-overlay-regressions.spec.ts.
const dialog = await openGroupedFaceDialog(frigateApp);
const triggers = dialog.locator('[aria-haspopup="menu"]');
await expect(triggers).toHaveCount(2);
await triggers.first().click();
let menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 5_000 });
await menu.getByRole("menuitem", { name: /^bob$/i }).click();
await expect(menu).not.toBeVisible({ timeout: 3_000 });
await triggers.nth(1).click();
menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 5_000 });
await frigateApp.page.keyboard.press("c");
await expect
.poll(
async () =>
frigateApp.page.evaluate(
() =>
document.activeElement?.textContent?.trim().toLowerCase() ?? "",
),
{ timeout: 2_000 },
)
.toMatch(/^charlie/);
await frigateApp.page.keyboard.press("Escape");
await expect(menu).not.toBeVisible({ timeout: 3_000 });
});
test("classifying the last image in a group leaves body interactive", async ({
frigateApp,
}) => {
// Regression guard for the stuck body pointer-events bug when the
// last image in a grouped-recognition detail Dialog is classified.
// Tracked upstream at radix-ui/primitives#3445.
//
// Root cause: when the user clicks a FaceSelectionDialog menu item,
// the modal DropdownMenu enters its exit animation (Radix's Presence
// keeps it in the DOM with data-state="closed" until animationend).
// While that is in flight the classify axios resolves, SWR removes
// the image from /api/faces, the parent's map no longer renders the
// grouped card, and React unmounts the subtree — including the still-
// animating DropdownMenu's Presence container. DismissableLayer's
// shared modal-layer stack can't reconcile the interrupted exit, so
// the `body { pointer-events: none }` entry it put on mount is never
// popped and the rest of the UI becomes unclickable.
//
// The fix is `modal={false}` on the FaceSelectionDialog's
// DropdownMenu (desktop path only). With modal=false the DropdownMenu
// never puts an entry on DismissableLayer's body-pointer-events stack
// in the first place, so there's nothing to leak when its Presence is
// torn down mid-animation. The Radix-community-documented workaround
// for #3445.
//
// The bug only reproduces when the mock resolves fast enough that
// the parent unmounts before the dropdown's exit animation finishes.
// Measured window via a 3x sweep on the pre-fix build: 0200 ms
// triggers it; 300 ms+ no longer reproduces. Production LAN networks
// sit comfortably inside the bad window, while `npm run dev` seems
// to mask it via React StrictMode's double-effect scheduling.
const EVENT_ID = "1775487131.3863528-race";
const initialFaces = withGroupedTrainingAttempt(basicFacesMock(), {
eventId: EVENT_ID,
attempts: [
{ timestamp: 1775487131.3863528, label: "unknown", score: 0.95 },
],
});
let classified = false;
await frigateApp.installDefaults({
faces: initialFaces,
events: [
{
id: EVENT_ID,
label: "person",
sub_label: null,
camera: "front_door",
start_time: 1775487131.3863528,
end_time: 1775487161.3863528,
false_positive: false,
zones: ["front_yard"],
thumbnail: null,
has_clip: true,
has_snapshot: true,
retain_indefinitely: false,
plus_id: null,
model_hash: "abc123",
detector_type: "cpu",
model_type: "ssd",
data: {
top_score: 0.92,
score: 0.92,
region: [0.1, 0.1, 0.5, 0.8],
box: [0.2, 0.15, 0.45, 0.75],
area: 0.18,
ratio: 0.6,
type: "object",
path_data: [],
},
},
],
});
// Re-route /api/faces to flip to the "train empty" payload once the
// classify POST has been received. Registered AFTER installDefaults so
// Playwright's LIFO route matching hits this handler first.
await frigateApp.page.route("**/api/faces", async (route) => {
const payload = classified ? basicFacesMock() : initialFaces;
await route.fulfill({ json: payload });
});
// Hold the classify POST briefly. The race opens when the parent
// unmounts before the dropdown's exit animation finishes (~200ms
// in Radix). 100ms keeps us comfortably inside that window and
// reliably triggered the bug in a 3x sweep across 0/50/100/200ms
// on the pre-fix build. CLASSIFY_DELAY_MS overrides for local sweeps.
const delayMs = Number(
(globalThis as { process?: { env?: Record<string, string> } }).process
?.env?.CLASSIFY_DELAY_MS ?? "100",
);
await frigateApp.page.route(
"**/api/faces/train/*/classify",
async (route) => {
classified = true;
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
await route.fulfill({ json: { success: true } });
},
);
await frigateApp.goto("/faces");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("#pageRoot button");
const count = await buttons.count();
expect(count).toBeGreaterThanOrEqual(0);
// Open the grouped detail Dialog.
const groupedImage = frigateApp.page
.locator('img[src*="clips/faces/train/"]')
.first();
await expect(groupedImage).toBeVisible({ timeout: 5_000 });
await groupedImage.locator("xpath=..").click();
const dialog = frigateApp.page
.getByRole("dialog")
.filter({
has: frigateApp.page.locator('img[src*="clips/faces/train/"]'),
})
.first();
await expect(dialog).toBeVisible({ timeout: 5_000 });
// Single attempt → single `+` trigger.
const triggers = dialog.locator('[aria-haspopup="menu"]');
await expect(triggers).toHaveCount(1);
await triggers.first().click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 5_000 });
await menu.getByRole("menuitem", { name: /^alice$/i }).click();
// The Dialog must leave the tree cleanly, and body must recover.
await expect(dialog).not.toBeVisible({ timeout: 5_000 });
// Give Radix's exit animation + cleanup a comfortable margin on top of
// the ~300ms simulated network delay.
await waitForBodyInteractive(frigateApp.page, 5_000);
await expectBodyInteractive(frigateApp.page);
// User-visible confirmation: click something outside the dialog
// and assert it actually responds.
const librarySelector = frigateApp.page
.getByRole("button")
.filter({ hasText: /\(\d+\)/ })
.first();
await librarySelector.click();
await expect(
frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first(),
).toBeVisible({ timeout: 3_000 });
});
});
test.describe("Face Library — mobile @high @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("mobile library selector dropdown closes cleanly on Escape", async ({
frigateApp,
}) => {
// Migrated from radix-overlay-regressions.spec.ts.
await installGroupedFaces(frigateApp);
await frigateApp.goto("/faces");
const selector = frigateApp.page
.getByRole("button")
.filter({ hasText: /\(\d+\)/ })
.first();
await expect(selector).toBeVisible({ timeout: 5_000 });
await selector.click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape");
await expect(menu).not.toBeVisible({ timeout: 3_000 });
await waitForBodyInteractive(frigateApp.page);
await expectBodyInteractive(frigateApp.page);
});
});
+237 -201
View File
@@ -1,59 +1,47 @@
/**
* Live page tests -- CRITICAL tier.
*
* Tests camera dashboard rendering, camera card clicks, single camera view
* with named controls, feature toggle behavior, context menu, and mobile layout.
* Dashboard grid, single-camera controls, feature toggles (with WS
* frame assertions), context menu, birdseye, and mobile layout.
* Also absorbs the PTZ preset-dropdown regression tests from the
* now-deleted ptz-overlay.spec.ts.
*/
import { test, expect } from "../fixtures/frigate-test";
import { LivePage } from "../pages/live.page";
import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
const PTZ_CAMERA = "front_door";
const PRESET_NAMES = ["home", "driveway", "front_porch"];
test.describe("Live Dashboard @critical", () => {
test("dashboard renders all configured cameras by name", async ({
test("every configured camera renders on the dashboard", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const live = new LivePage(frigateApp.page, !frigateApp.isMobile);
for (const cam of ["front_door", "backyard", "garage"]) {
await expect(
frigateApp.page.locator(`[data-camera='${cam}']`),
).toBeVisible({ timeout: 10_000 });
await expect(live.cameraCard(cam)).toBeVisible({ timeout: 10_000 });
}
});
test("clicking camera card opens single camera view via hash", async ({
test("clicking a camera card opens the single-camera view via hash", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
const live = new LivePage(frigateApp.page, !frigateApp.isMobile);
await live.cameraCard("front_door").first().click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
test("back button returns from single camera to dashboard", async ({
frigateApp,
}) => {
// First navigate to dashboard so there's history to go back to
await frigateApp.goto("/");
await frigateApp.page.waitForTimeout(1000);
// Click a camera to enter single view
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
await frigateApp.page.waitForTimeout(2000);
// Now click Back to return to dashboard
const backBtn = frigateApp.page.getByText("Back", { exact: true });
if (await backBtn.isVisible().catch(() => false)) {
await backBtn.click();
await frigateApp.page.waitForTimeout(1000);
}
// Should be back on the dashboard with cameras visible
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("birdseye view loads without crash", async ({ frigateApp }) => {
test("birdseye route renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/#birdseye");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("empty group shows fallback content", async ({ frigateApp }) => {
@@ -63,191 +51,239 @@ test.describe("Live Dashboard @critical", () => {
});
});
test.describe("Live Single Camera - Controls @critical", () => {
test("single camera view shows Back and History buttons (desktop)", async ({
test.describe("Live Single Camera — desktop controls @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Desktop-only header controls",
);
test("single-camera view shows Back and History buttons", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip(); // On mobile, buttons may show icons only
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Back and History are visible text buttons in the header
await expect(
frigateApp.page.getByText("Back", { exact: true }),
).toBeVisible({ timeout: 5_000 });
await expect(
frigateApp.page.getByText("History", { exact: true }),
).toBeVisible();
const live = new LivePage(frigateApp.page, true);
await expect(live.backButton).toBeVisible({ timeout: 5_000 });
await expect(live.historyButton).toBeVisible();
});
test("single camera view shows feature toggle icons (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test("feature toggles render (at least 3)", async ({ frigateApp }) => {
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Feature toggles are CameraFeatureToggle components rendered as divs
// with bg-selected (active) or bg-secondary (inactive) classes
// Count the toggles - should have at least detect, recording, snapshots
const toggles = frigateApp.page.locator(
".flex.flex-col.items-center.justify-center.bg-selected, .flex.flex-col.items-center.justify-center.bg-secondary",
);
const count = await toggles.count();
const live = new LivePage(frigateApp.page, true);
// Wait for the single-camera header to render before counting toggles.
await expect(live.backButton).toBeVisible({ timeout: 5_000 });
await expect(live.featureToggles.first()).toBeVisible({ timeout: 5_000 });
const count = await live.featureToggles.count();
expect(count).toBeGreaterThanOrEqual(3);
});
test("clicking a feature toggle changes its visual state (desktop)", async ({
test("clicking a feature toggle sends the matching WS frame", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await installWsFrameCapture(frigateApp.page);
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Find active toggles (bg-selected class = feature is ON)
const activeToggles = frigateApp.page.locator(
".flex.flex-col.items-center.justify-center.bg-selected",
const live = new LivePage(frigateApp.page, true);
// Wait for feature toggles to render (WS camera_activity must arrive first).
await expect(live.activeFeatureToggles.first()).toBeVisible({
timeout: 5_000,
});
const activeBefore = await live.activeFeatureToggles.count();
expect(activeBefore).toBeGreaterThan(0);
await live.activeFeatureToggles.first().click();
// The toggle dispatches a frame on <camera>/<feature>/set — match on
// front_door/ prefix + /set suffix (any feature).
await waitForWsFrame(
frigateApp.page,
(frame) => frame.includes("front_door/") && frame.includes("/set"),
{
message:
"feature toggle should dispatch a <camera>/<feature>/set frame",
},
);
const initialCount = await activeToggles.count();
if (initialCount > 0) {
// Click the first active toggle to disable it
await activeToggles.first().click();
await frigateApp.page.waitForTimeout(1000);
// After WS mock echoes back new state, count should decrease
const newCount = await activeToggles.count();
expect(newCount).toBeLessThan(initialCount);
}
});
test("settings gear button opens dropdown (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Find the gear icon button (last button-like element in header)
// The settings gear opens a dropdown with Stream, Play in background, etc.
const gearButtons = frigateApp.page.locator("button:has(svg)");
const count = await gearButtons.count();
// Click the last one (gear icon is typically last in the header)
if (count > 0) {
await gearButtons.last().click();
await frigateApp.page.waitForTimeout(500);
// A dropdown or drawer should appear
const overlay = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content], [role="dialog"]',
);
const visible = await overlay
.first()
.isVisible()
.catch(() => false);
if (visible) {
await frigateApp.page.keyboard.press("Escape");
}
}
});
test("keyboard shortcut f does not crash on desktop", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test("keyboard shortcut f does not crash", async ({ frigateApp }) => {
await frigateApp.goto("/");
await frigateApp.page.keyboard.press("f");
await frigateApp.page.waitForTimeout(500);
await expect(frigateApp.page.locator("body")).toBeVisible();
// Note: headless Chromium rejects fullscreen requests without a user
// gesture, so document.fullscreenElement cannot be asserted reliably
// in e2e. We assert the keypress doesn't crash the app; real
// fullscreen behavior is covered by manual testing.
});
test("settings gear opens a dropdown with Stream/Play menu items", async ({
frigateApp,
}) => {
await frigateApp.goto("/#front_door");
// Wait for the single-camera view to render — use the Back button
// as a deterministic marker.
const live = new LivePage(frigateApp.page, true);
await expect(live.backButton).toBeVisible({ timeout: 10_000 });
// The gear icon button is the last button-like element in the
// single-camera header. Clicking it opens a Radix dropdown.
const gearButtons = frigateApp.page.locator("button:has(svg)");
const count = await gearButtons.count();
expect(count).toBeGreaterThan(0);
await gearButtons.last().click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape");
await expect(menu).not.toBeVisible({ timeout: 3_000 });
});
});
test.describe("Live Context Menu (desktop) @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Right-click is desktop-only",
);
test("right-click opens the context menu", async ({ frigateApp }) => {
await frigateApp.goto("/");
const live = new LivePage(frigateApp.page, true);
const menu = await live.openContextMenuOn("front_door");
await expect(menu).toBeVisible({ timeout: 5_000 });
});
test("context menu closes on Escape and leaves body interactive", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const live = new LivePage(frigateApp.page, true);
const menu = await live.openContextMenuOn("front_door");
await expect(menu).toBeVisible({ timeout: 5_000 });
await frigateApp.page.keyboard.press("Escape");
await expect(menu).not.toBeVisible();
await waitForBodyInteractive(frigateApp.page);
await expectBodyInteractive(frigateApp.page);
});
});
test.describe("Live PTZ preset dropdown @critical", () => {
// Migrated from ptz-overlay.spec.ts. Guards:
// 1. After selecting a preset, the "Presets" tooltip must not re-pop.
// 2. Keyboard shortcuts after close should not re-open the dropdown.
test("selecting a preset closes menu cleanly and does not re-open on keyboard", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "PTZ preset dropdown is desktop-only");
await frigateApp.api.install({
config: {
cameras: {
[PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } },
},
},
});
await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
route.fulfill({
json: {
name: PTZ_CAMERA,
features: ["pt", "zoom"],
presets: PRESET_NAMES,
profiles: [],
},
}),
);
await installWsFrameCapture(frigateApp.page);
await frigateApp.goto(`/#${PTZ_CAMERA}`);
const presetTrigger = frigateApp.page.getByRole("button", {
name: /presets/i,
});
await expect(presetTrigger.first()).toBeVisible({ timeout: 5_000 });
await presetTrigger.first().click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 3_000 });
await menu.getByRole("menuitem", { name: PRESET_NAMES[0] }).first().click();
await expect(menu).not.toBeVisible({ timeout: 3_000 });
await waitForWsFrame(
frigateApp.page,
(frame) =>
frame.includes(`"${PTZ_CAMERA}/ptz"`) &&
frame.includes(`preset_${PRESET_NAMES[0]}`),
);
await waitForBodyInteractive(frigateApp.page);
await expectBodyInteractive(frigateApp.page);
await expect
.poll(
async () =>
frigateApp.page
.locator('[role="tooltip"]')
.filter({ hasText: /presets/i })
.isVisible()
.catch(() => false),
{ timeout: 1_000 },
)
.toBe(false);
await frigateApp.page.keyboard.press("ArrowUp");
await frigateApp.page.keyboard.press("Space");
await frigateApp.page.keyboard.press("Enter");
await expect
.poll(() => menu.isVisible().catch(() => false), { timeout: 1_000 })
.toBe(false);
});
});
test.describe("Live mobile layout @critical @mobile", () => {
test("mobile dashboard has no sidebar and renders cameras", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).toHaveCount(0);
const live = new LivePage(frigateApp.page, false);
await expect(live.cameraCard("front_door")).toBeVisible({
timeout: 10_000,
});
});
test("mobile camera tap opens single view", async ({ frigateApp }) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto("/");
const live = new LivePage(frigateApp.page, false);
await live.cameraCard("front_door").first().click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
test("mobile onvif single-camera view loads without freezing body", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
// Migrated from ptz-overlay.spec.ts — dismissable-layer dedupe smoke test.
await frigateApp.api.install({
config: {
cameras: { [PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } } },
},
});
await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
route.fulfill({
json: {
name: PTZ_CAMERA,
features: ["pt", "zoom"],
presets: PRESET_NAMES,
profiles: [],
},
}),
);
await frigateApp.goto(`/#${PTZ_CAMERA}`);
await expectBodyInteractive(frigateApp.page);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
test.describe("Live Single Camera - Mobile Controls @critical", () => {
test("mobile camera view has settings drawer trigger", async ({
frigateApp,
}) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// On mobile, settings gear opens a drawer
// The button has aria-label with the camera name like "front_door Settings"
const buttons = frigateApp.page.locator("button:has(svg)");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
});
test.describe("Live Context Menu @critical", () => {
test("right-click on camera opens context menu on desktop", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.waitFor({ state: "visible", timeout: 10_000 });
await card.click({ button: "right" });
const contextMenu = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content]',
);
await expect(contextMenu.first()).toBeVisible({ timeout: 5_000 });
});
test("context menu closes on escape", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.waitFor({ state: "visible", timeout: 10_000 });
await card.click({ button: "right" });
await frigateApp.page.waitForTimeout(500);
await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300);
const contextMenu = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content]',
);
await expect(contextMenu).not.toBeVisible();
});
});
test.describe("Live Mobile Layout @critical", () => {
test("mobile renders cameras without sidebar", async ({ frigateApp }) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).not.toBeVisible();
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("mobile camera click opens single camera view", async ({
frigateApp,
}) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
});
+209 -62
View File
@@ -1,75 +1,222 @@
/**
* Logs page tests -- MEDIUM tier.
*
* Tests service tab switching by name, copy/download buttons,
* and websocket message feed tab.
* Service tabs (with real /logs/<service> JSON contract),
* log content render, Copy (clipboard), Download (assert
* ?download=true request fired), mobile tab selector.
*/
import { test, expect } from "../fixtures/frigate-test";
import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
test.describe("Logs Page - Service Tabs @medium", () => {
test("logs page renders with named service tabs", async ({ frigateApp }) => {
function logsJsonBody(lines: string[]) {
return { lines, totalLines: lines.length };
}
test.describe("Logs — service tabs @medium", () => {
test("frigate tab renders by default with mocked log lines", async ({
frigateApp,
}) => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({
json: logsJsonBody([
"[2026-04-06 10:00:00] INFO: Frigate started",
"[2026-04-06 10:00:01] INFO: Cameras loaded",
]),
}),
);
// Silence the streaming fetch so it doesn't hang the test.
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
timeout: 10_000,
});
});
test("switching to go2rtc fires a GET to /logs/go2rtc", async ({
frigateApp,
}) => {
let go2rtcCalled = false;
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({ json: logsJsonBody(["frigate line"]) }),
);
await frigateApp.page.route(/\/api\/logs\/go2rtc(\?|$)/, (route) => {
if (!route.request().url().includes("stream=true")) {
go2rtcCalled = true;
}
return route.fulfill({ json: logsJsonBody(["go2rtc line"]) });
});
await frigateApp.page.route(/\/api\/logs\/.*\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc");
await expect(go2rtcTab).toBeVisible();
await go2rtcTab.click();
await expect.poll(() => go2rtcCalled, { timeout: 5_000 }).toBe(true);
await expect(go2rtcTab).toHaveAttribute("data-state", "on");
});
});
test.describe("Logs — actions @medium", () => {
test("Copy button writes current logs to clipboard", async ({
frigateApp,
context,
}) => {
await grantClipboardPermissions(context);
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({
json: logsJsonBody([
"[2026-04-06 10:00:00] INFO: Frigate started",
"[2026-04-06 10:00:01] INFO: Cameras loaded",
]),
}),
);
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
timeout: 10_000,
});
const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
await expect(copyBtn).toBeVisible({ timeout: 5_000 });
await copyBtn.click();
await expect
.poll(() => readClipboard(frigateApp.page), { timeout: 5_000 })
.toContain("Frigate started");
});
test("Download button fires GET /logs/<service>?download=true", async ({
frigateApp,
}) => {
let downloadCalled = false;
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => {
if (route.request().url().includes("download=true")) {
downloadCalled = true;
}
return route.fulfill({ json: logsJsonBody(["frigate line"]) });
});
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
const downloadBtn = frigateApp.page.getByLabel("Download Logs");
await expect(downloadBtn).toBeVisible({ timeout: 5_000 });
await downloadBtn.click();
await expect.poll(() => downloadCalled, { timeout: 5_000 }).toBe(true);
});
});
test.describe("Logs — websocket tab @medium", () => {
test("switching to websocket tab renders WsMessageFeed container", async ({
frigateApp,
}) => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({ json: logsJsonBody(["frigate line"]) }),
);
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
const wsTab = frigateApp.page.getByLabel("Select websocket");
await expect(wsTab).toBeVisible({ timeout: 5_000 });
await wsTab.click();
await expect(wsTab).toHaveAttribute("data-state", "on", { timeout: 5_000 });
});
});
test.describe("Logs — streaming @medium", () => {
test("streamed log lines appear in the viewport", async ({ frigateApp }) => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => {
if (route.request().url().includes("stream=true")) {
// Intercepted below via addInitScript fetch override.
return route.fallback();
}
return route.fulfill({
json: logsJsonBody(["[2026-04-06 10:00:00] INFO: initial batch line"]),
});
});
// Override window.fetch so the /api/logs/frigate?stream=true request
// resolves with a real ReadableStream that emits chunks over time.
// This is the only way to validate streaming-append behavior through
// Playwright — route.fulfill() cannot return a stream.
// NOTE: The app calls fetch('api/logs/...') with a relative URL (no
// leading slash), so we match both relative and absolute forms.
await frigateApp.page.addInitScript(() => {
const origFetch = window.fetch;
window.fetch = async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: (input as Request).url;
if (url.includes("api/logs/frigate") && url.includes("stream=true")) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await new Promise((r) => setTimeout(r, 30));
controller.enqueue(
encoder.encode(
"[2026-04-06 10:00:02] INFO: streamed line one\n",
),
);
await new Promise((r) => setTimeout(r, 30));
controller.enqueue(
encoder.encode(
"[2026-04-06 10:00:03] INFO: streamed line two\n",
),
);
controller.close();
},
});
return new Response(stream, { status: 200 });
}
return origFetch.call(window, input as RequestInfo, init);
};
});
await frigateApp.goto("/logs");
// The initial batch line is parsed by LogLineData and its content is
// rendered in a .log-content cell — assert against that element.
await expect(frigateApp.page.getByText("initial batch line")).toBeVisible({
timeout: 10_000,
});
await expect(frigateApp.page.getByText(/streamed line one/)).toBeVisible({
timeout: 10_000,
});
await expect(frigateApp.page.getByText(/streamed line two/)).toBeVisible({
timeout: 10_000,
});
});
});
test.describe("Logs — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("service tabs render at mobile viewport", async ({ frigateApp }) => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({ json: logsJsonBody(["frigate line"]) }),
);
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
route.fulfill({ status: 200, body: "" }),
);
await frigateApp.goto("/logs");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
// Service tabs have aria-label="Select {service}"
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
test("switching to go2rtc tab changes active tab", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc");
if (await go2rtcTab.isVisible().catch(() => false)) {
await go2rtcTab.click();
await frigateApp.page.waitForTimeout(1000);
await expect(go2rtcTab).toHaveAttribute("data-state", "on");
}
});
test("switching to websocket tab shows message feed", async ({
frigateApp,
}) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const wsTab = frigateApp.page.getByLabel("Select websocket");
if (await wsTab.isVisible().catch(() => false)) {
await wsTab.click();
await frigateApp.page.waitForTimeout(1000);
await expect(wsTab).toHaveAttribute("data-state", "on");
}
});
});
test.describe("Logs Page - Actions @medium", () => {
test("copy to clipboard button is present and clickable", async ({
frigateApp,
}) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
if (await copyBtn.isVisible().catch(() => false)) {
await copyBtn.click();
await frigateApp.page.waitForTimeout(500);
// Should trigger clipboard copy (toast may appear)
}
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("download logs button is present", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const downloadBtn = frigateApp.page.getByLabel("Download Logs");
if (await downloadBtn.isVisible().catch(() => false)) {
await expect(downloadBtn).toBeVisible();
}
});
test("logs page displays log content text", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(2000);
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
});
});
+110 -162
View File
@@ -1,227 +1,175 @@
/**
* Navigation tests -- CRITICAL tier.
*
* Tests sidebar (desktop) and bottombar (mobile) navigation,
* conditional nav items, settings menus, and their actual behaviors.
* Covers sidebar (desktop) / bottombar (mobile) link set, conditional
* nav items (faces, chat, classification), settings menu navigation,
* unknown-route redirect to /, and mobile-specific nav behaviors.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
test.describe("Navigation @critical", () => {
test("app loads and renders page root", async ({ frigateApp }) => {
await frigateApp.goto("/");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
const PRIMARY_ROUTES = ["/review", "/explore", "/export"] as const;
test("logo is visible and links to home", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, true);
const logo = base.sidebar.locator('a[href="/"]').first();
await expect(logo).toBeVisible();
});
test("all primary nav links are present and navigate", async ({
test.describe("Navigation — primary links @critical", () => {
test("every primary link is visible and navigates", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const routes = ["/review", "/explore", "/export"];
for (const route of routes) {
for (const route of PRIMARY_ROUTES) {
await expect(
frigateApp.page.locator(`a[href="${route}"]`).first(),
).toBeVisible();
}
// Verify clicking each one actually navigates
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
for (const route of routes) {
for (const route of PRIMARY_ROUTES) {
await base.navigateTo(route);
await expect(frigateApp.page).toHaveURL(new RegExp(route));
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}
});
test("desktop sidebar is visible, mobile bottombar is visible", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
if (!frigateApp.isMobile) {
await expect(base.sidebar).toBeVisible();
} else {
await expect(base.sidebar).not.toBeVisible();
}
test("logo links home on desktop", async ({ frigateApp }) => {
test.skip(frigateApp.isMobile, "Sidebar logo is desktop-only");
await frigateApp.goto("/review");
await frigateApp.page.locator("aside a[href='/']").first().click();
await expect(frigateApp.page).toHaveURL(/\/$/);
});
test("navigate between all main pages without crash", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
const pageRoot = frigateApp.page.locator("#pageRoot");
await base.navigateTo("/review");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/explore");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/export");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/review");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
});
test("unknown route redirects to home", async ({ frigateApp }) => {
test("unknown route redirects to /", async ({ frigateApp }) => {
await frigateApp.page.goto("/nonexistent-route");
await frigateApp.page.waitForTimeout(2000);
const url = frigateApp.page.url();
const hasPageRoot = await frigateApp.page
.locator("#pageRoot")
.isVisible()
.catch(() => false);
expect(url.endsWith("/") || hasPageRoot).toBeTruthy();
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/\/$/);
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
});
test.describe("Navigation - Conditional Items @critical", () => {
test("Faces nav hidden when face_recognition disabled", async ({
test.describe("Navigation — conditional items @critical", () => {
test("/faces is hidden when face_recognition.enabled is false", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
await expect(frigateApp.page.locator('a[href="/faces"]')).not.toBeVisible();
await expect(
frigateApp.page.locator('a[href="/faces"]').first(),
).toHaveCount(0);
});
test("Chat nav hidden when genai model is none", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.installDefaults({
config: {
genai: {
enabled: false,
provider: "ollama",
model: "none",
base_url: "",
},
},
});
await frigateApp.goto("/");
await expect(frigateApp.page.locator('a[href="/chat"]')).not.toBeVisible();
});
test("Faces nav visible when face_recognition enabled on desktop", async ({
test("/faces is visible when face_recognition.enabled is true (desktop)", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({
config: { face_recognition: { enabled: true } },
});
await frigateApp.goto("/");
await expect(page.locator('a[href="/faces"]')).toBeVisible();
await expect(
frigateApp.page.locator('a[href="/faces"]').first(),
).toBeVisible();
});
test("Chat nav visible when genai model set on desktop", async ({
test("/chat is hidden when no agent has the chat role (desktop)", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({
config: { genai: { enabled: true, model: "llava" } },
config: {
genai: {
descriptions_only: {
provider: "ollama",
model: "llava",
roles: ["descriptions"],
},
},
},
});
await frigateApp.goto("/");
await expect(page.locator('a[href="/chat"]')).toBeVisible();
await expect(
frigateApp.page.locator('a[href="/chat"]').first(),
).toHaveCount(0);
});
test("Classification nav visible for admin on desktop", async ({
test("/chat is visible when an agent has the chat role (desktop)", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({
config: {
genai: {
chat_agent: {
provider: "ollama",
model: "llava",
roles: ["chat"],
},
},
},
});
await frigateApp.goto("/");
await expect(page.locator('a[href="/classification"]')).toBeVisible();
await expect(
frigateApp.page.locator('a[href="/chat"]').first(),
).toBeVisible();
});
test("/classification is visible for admin on desktop", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.goto("/");
await expect(
frigateApp.page.locator('a[href="/classification"]').first(),
).toBeVisible();
});
});
test.describe("Navigation - Settings Menu @critical", () => {
test("settings gear opens menu with navigation items (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
// Settings gear is in the sidebar bottom section, a div with cursor-pointer
const sidebarBottom = frigateApp.page.locator("aside .mb-8");
const gearIcon = sidebarBottom
.locator("div[class*='cursor-pointer']")
.first();
await expect(gearIcon).toBeVisible({ timeout: 5_000 });
await gearIcon.click();
// Menu should open - look for the "Settings" menu item by aria-label
await expect(frigateApp.page.getByLabel("Settings")).toBeVisible({
timeout: 3_000,
});
});
test.describe("Navigation — settings menu (desktop) @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Sidebar settings menu is desktop-only",
);
test("settings menu items navigate to correct routes (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
const targets = [
{ label: "Settings", url: "/settings" },
{ label: "System metrics", url: "/system" },
{ label: "System logs", url: "/logs" },
{ label: "Configuration Editor", url: "/config" },
];
for (const target of targets) {
const TARGETS = [
{ label: "Settings", url: /\/settings/ },
{ label: "System metrics", url: /\/system/ },
{ label: "System logs", url: /\/logs/ },
{ label: "Configuration Editor", url: /\/config/ },
];
for (const target of TARGETS) {
test(`menu → ${target.label} navigates`, async ({ frigateApp }) => {
await frigateApp.goto("/");
const gearIcon = frigateApp.page
const gear = frigateApp.page
.locator("aside .mb-8 div[class*='cursor-pointer']")
.first();
await gearIcon.click();
await frigateApp.page.waitForTimeout(300);
const menuItem = frigateApp.page.getByLabel(target.label);
if (await menuItem.isVisible().catch(() => false)) {
await menuItem.click();
await expect(frigateApp.page).toHaveURL(
new RegExp(target.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
}
await gear.click();
await frigateApp.page.getByLabel(target.label).click();
await expect(frigateApp.page).toHaveURL(target.url);
});
}
});
test.describe("Navigation — mobile @critical @mobile", () => {
test("mobile bottombar visible, sidebar not rendered", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).toHaveCount(0);
for (const route of PRIMARY_ROUTES) {
await expect(
frigateApp.page.locator(`a[href="${route}"]`).first(),
).toBeVisible();
}
});
test("account button in sidebar is clickable (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
test("mobile nav survives route change", async ({ frigateApp }) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto("/");
const sidebarBottom = frigateApp.page.locator("aside .mb-8");
const items = sidebarBottom.locator("div[class*='cursor-pointer']");
const count = await items.count();
if (count >= 2) {
await items.nth(1).click();
await frigateApp.page.waitForTimeout(500);
}
await expect(frigateApp.page.locator("body")).toBeVisible();
const reviewLink = frigateApp.page.locator('a[href="/review"]').first();
await reviewLink.click();
await expect(frigateApp.page).toHaveURL(/\/review/);
await expect(
frigateApp.page.locator('a[href="/review"]').first(),
).toBeVisible();
});
});
+292 -11
View File
@@ -1,23 +1,304 @@
/**
* Replay page tests -- LOW tier.
* Replay page tests -- MEDIUM tier.
*
* Tests replay page rendering and basic interactivity.
* /replay is the admin debug replay page (not a recordings player).
* Polls /api/debug_replay/status, renders a no-session state when
* inactive, and a live camera image + debug toggles + Stop controls
* when active.
*/
import { test, expect } from "../fixtures/frigate-test";
import {
activeSessionStatus,
noSessionStatus,
} from "../fixtures/mock-data/debug-replay";
test.describe("Replay Page @low", () => {
test("replay page renders without crash", async ({ frigateApp }) => {
async function installStatusRoute(
app: { page: import("@playwright/test").Page },
body: unknown,
) {
await app.page.route("**/api/debug_replay/status", (route) =>
route.fulfill({ json: body }),
);
}
test.describe("Replay — no active session @medium", () => {
test("empty state renders heading + Go to History button", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, noSessionStatus());
await frigateApp.goto("/replay");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /No Active Debug Replay Session/i,
}),
).toBeVisible({ timeout: 10_000 });
const goButton = frigateApp.page.getByRole("button", {
name: /Go to History|Go to Recordings/i,
});
await expect(goButton).toBeVisible();
});
test("replay page has interactive controls", async ({ frigateApp }) => {
test("clicking Go to History navigates to /review", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, noSessionStatus());
await frigateApp.goto("/replay");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("button");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /No Active Debug Replay Session/i,
}),
).toBeVisible({ timeout: 10_000 });
await frigateApp.page
.getByRole("button", { name: /Go to History|Go to Recordings/i })
.click();
await expect(frigateApp.page).toHaveURL(/\/review/);
});
});
test.describe("Replay — active session @medium", () => {
test("active status renders the Debug Replay side panel", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
// Three tabs (Debug / Objects / Messages) in TabsList
await expect(frigateApp.page.locator('[role="tab"]')).toHaveCount(3);
});
test("debug toggles render with bbox ON by default", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
const bbox = frigateApp.page.locator("#debug-bbox");
await expect(bbox).toBeVisible({ timeout: 10_000 });
await expect(bbox).toHaveAttribute("aria-checked", "true");
});
test("clicking bbox toggle flips aria-checked", async ({ frigateApp }) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
const bbox = frigateApp.page.locator("#debug-bbox");
await expect(bbox).toBeVisible({ timeout: 10_000 });
await expect(bbox).toHaveAttribute("aria-checked", "true");
await bbox.click();
await expect(bbox).toHaveAttribute("aria-checked", "false");
});
test("Configuration button opens the configuration dialog (desktop)", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Desktop: button has visible text label");
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
// On desktop the span is visible and gives the button an accessible name.
await frigateApp.page
.getByRole("button", { name: /configuration/i })
.first()
.click();
const dialog = frigateApp.page.getByRole("dialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
});
test("Configuration button opens the configuration dialog (mobile)", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile: button is icon-only");
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
// On mobile the Configuration button text span is hidden (md:inline).
// It is the first button inside the right-side action group div
// (the flex container that holds Config + Stop, sibling of the Back button).
const actionGroup = frigateApp.page.locator(
".flex.items-center.gap-2 button",
);
await actionGroup.first().click();
const dialog = frigateApp.page.getByRole("dialog");
await expect(dialog).toBeVisible({ timeout: 5_000 });
});
test("Objects tab renders with the camera_activity objects list", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
// Send an activity payload with a person object on front_door.
// Must be called after goto() so the WS connection is established.
await frigateApp.ws.sendCameraActivity({
front_door: {
objects: [
{
label: "person",
score: 0.95,
box: [0.1, 0.1, 0.5, 0.8],
area: 0.2,
ratio: 0.6,
region: [0.05, 0.05, 0.6, 0.85],
current_zones: [],
id: "obj-person-1",
},
],
},
});
// Switch to Objects tab (labelled "Object List" in i18n).
const objectsTab = frigateApp.page.getByRole("tab", {
name: /object/i,
});
await objectsTab.click();
await expect(objectsTab).toHaveAttribute("data-state", "active", {
timeout: 3_000,
});
// The object row renders the label.
await expect(frigateApp.page.getByText(/person/i).first()).toBeVisible({
timeout: 5_000,
});
});
test("Messages tab renders WsMessageFeed container", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
const messagesTab = frigateApp.page.getByRole("tab", {
name: /messages/i,
});
await messagesTab.click();
await expect(messagesTab).toHaveAttribute("data-state", "active", {
timeout: 3_000,
});
});
test("bbox info popover opens and closes cleanly", async ({ frigateApp }) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
// The bbox row has an info icon popover trigger next to its label.
// The trigger is a div (not button) wrapping LuInfo with an sr-only
// "Info" span. Target it by the sr-only text content.
const infoTrigger = frigateApp.page
.locator("span.sr-only", { hasText: /info/i })
.first();
await expect(infoTrigger).toBeVisible({ timeout: 10_000 });
// Click the parent div (the actual trigger)
await infoTrigger.locator("..").click();
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape");
await expect(popover.first()).not.toBeVisible({ timeout: 3_000 });
});
});
test.describe("Replay — stop flow (desktop) @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Desktop button has accessible 'Stop Replay' name",
);
test("Stop Replay opens confirm dialog; confirm POSTs debug_replay/stop", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
let stopCalled = false;
await frigateApp.page.route("**/api/debug_replay/stop", async (route) => {
if (route.request().method() === "POST") stopCalled = true;
await route.fulfill({ json: { success: true } });
});
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
await frigateApp.page
.getByRole("button", { name: /stop replay/i })
.first()
.click();
const dialog = frigateApp.page.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 3_000 });
await dialog
.getByRole("button", { name: /stop|confirm/i })
.first()
.click();
await expect.poll(() => stopCalled, { timeout: 5_000 }).toBe(true);
});
});
test.describe("Replay — stop button (mobile) @medium @mobile", () => {
test.skip(
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only icon-button variant",
);
test("tapping the icon-only stop button opens the confirm dialog", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, activeSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 });
// On mobile the Stop button is an icon (LuSquare) inside an
// AlertDialogTrigger. It's the last button in the top bar's
// right-side action group (Back is on the left). Target by
// position within the top-bar flex container.
const topRightButtons = frigateApp.page
.locator(".min-h-12 button, .md\\:min-h-16 button")
.filter({ hasNot: frigateApp.page.getByLabel("Back") });
const lastButton = topRightButtons.last();
await expect(lastButton).toBeVisible({ timeout: 10_000 });
await lastButton.click();
const dialog = frigateApp.page.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 3_000 });
await dialog.getByRole("button", { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 3_000 });
});
});
test.describe("Replay — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("no-session state renders at mobile viewport", async ({
frigateApp,
}) => {
await installStatusRoute(frigateApp, noSessionStatus());
await frigateApp.goto("/replay");
await expect(
frigateApp.page.getByRole("heading", {
level: 2,
name: /No Active Debug Replay Session/i,
}),
).toBeVisible({ timeout: 10_000 });
});
});
+188 -158
View File
@@ -1,200 +1,230 @@
/**
* Review/Events page tests -- CRITICAL tier.
*
* Tests severity tab switching by name (Alerts/Detections/Motion),
* filter popover opening with camera names, show reviewed toggle,
* calendar button, and filter button interactions.
* Severity tabs, filter popovers, calendar, show-reviewed toggle,
* timeline, and the nested-overlay regression migrated from
* radix-overlay-regressions.spec.ts.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
import { ReviewPage } from "../pages/review.page";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
test.describe("Review Page - Severity Tabs @critical", () => {
test("severity tabs render with Alerts, Detections, Motion", async ({
test.describe("Review — severity tabs @critical", () => {
test("tabs render with Alerts default-on", async ({ frigateApp }) => {
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
await expect(review.detectionsTab).toBeVisible();
await expect(review.motionTab).toBeVisible();
await expect(review.alertsTab).toHaveAttribute("data-state", "on");
});
test("clicking Detections flips data-state", async ({ frigateApp }) => {
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
await review.detectionsTab.click();
await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
await expect(review.alertsTab).toHaveAttribute("data-state", "off");
});
test("clicking Motion flips data-state", async ({ frigateApp }) => {
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
await review.motionTab.click();
await expect(review.motionTab).toHaveAttribute("data-state", "on");
});
test("switching back to Alerts works", async ({ frigateApp }) => {
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
await review.detectionsTab.click();
await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
await review.alertsTab.click();
await expect(review.alertsTab).toHaveAttribute("data-state", "on");
});
test("switching tabs updates active data-state (client-side filter)", async ({
frigateApp,
}) => {
// The severity tabs filter the already-fetched review data client-side;
// they do not trigger a new /api/review network request. This test
// verifies the state-change assertion that the tab switch takes effect.
await frigateApp.goto("/review");
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
await expect(frigateApp.page.getByLabel("Detections")).toBeVisible();
// Motion uses role="radio" to distinguish from other Motion elements
await expect(
frigateApp.page.getByRole("radio", { name: "Motion" }),
).toBeVisible();
});
test("Alerts tab is active by default", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const alertsTab = frigateApp.page.getByLabel("Alerts");
await expect(alertsTab).toHaveAttribute("data-state", "on");
});
test("clicking Detections tab makes it active and deactivates Alerts", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const alertsTab = frigateApp.page.getByLabel("Alerts");
const detectionsTab = frigateApp.page.getByLabel("Detections");
await detectionsTab.click();
await frigateApp.page.waitForTimeout(500);
await expect(detectionsTab).toHaveAttribute("data-state", "on");
await expect(alertsTab).toHaveAttribute("data-state", "off");
});
test("clicking Motion tab makes it active", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const motionTab = frigateApp.page.getByRole("radio", { name: "Motion" });
await motionTab.click();
await frigateApp.page.waitForTimeout(500);
await expect(motionTab).toHaveAttribute("data-state", "on");
});
test("switching back to Alerts from Detections works", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
await frigateApp.page.getByLabel("Detections").click();
await frigateApp.page.waitForTimeout(300);
await frigateApp.page.getByLabel("Alerts").click();
await frigateApp.page.waitForTimeout(300);
await expect(frigateApp.page.getByLabel("Alerts")).toHaveAttribute(
"data-state",
"on",
);
const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
await expect(review.alertsTab).toHaveAttribute("data-state", "on");
await review.detectionsTab.click();
await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
await expect(review.alertsTab).toHaveAttribute("data-state", "off");
});
});
test.describe("Review Page - Filters @critical", () => {
test("All Cameras filter button opens popover with camera names", async ({
test.describe("Review — filters (desktop) @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Filter bar differs on mobile",
);
test("Cameras popover lists configured camera names", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i,
});
await expect(camerasBtn).toBeVisible({ timeout: 5_000 });
await camerasBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover should open with camera names from config
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Camera names should be present
const review = new ReviewPage(frigateApp.page, true);
await expect(review.camerasFilterTrigger).toBeVisible({ timeout: 5_000 });
await review.camerasFilterTrigger.click();
await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 });
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
});
test("closing the Cameras popover with Escape leaves body interactive", async ({
frigateApp,
}) => {
// Migrated from radix-overlay-regressions.spec.ts.
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, true);
await review.camerasFilterTrigger.click();
await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape");
await expect(review.filterOverlay).not.toBeVisible({ timeout: 3_000 });
await waitForBodyInteractive(frigateApp.page);
await expectBodyInteractive(frigateApp.page);
});
test("Labels are shown inside the General Filter dialog", async ({
frigateApp,
}) => {
// Labels are surfaced inside the "Filter" button's GeneralFilterContent
// dialog, not as a standalone top-level button. We open that dialog and
// confirm labels from the camera config are listed there.
await frigateApp.goto("/review");
const filterBtn = frigateApp.page
.getByRole("button", { name: /^filter$/i })
.first();
await expect(filterBtn).toBeVisible({ timeout: 5_000 });
await filterBtn.click();
const overlay = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog']",
);
await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
// The default mock config for front_door tracks "person"
await expect(overlay.first().getByText(/person/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
test("Show Reviewed toggle is clickable", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const showReviewed = frigateApp.page.getByRole("button", {
name: /reviewed/i,
test("Zones popover lists configured zones inside the General Filter dialog", async ({
frigateApp,
}) => {
// Override config to guarantee a known zone on front_door.
await frigateApp.installDefaults({
config: {
cameras: {
front_door: {
zones: {
front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" },
},
},
},
},
});
if (await showReviewed.isVisible().catch(() => false)) {
await showReviewed.click();
await frigateApp.page.waitForTimeout(500);
// Toggle should change state
await expect(frigateApp.page.locator("body")).toBeVisible();
}
await frigateApp.goto("/review");
const filterBtn = frigateApp.page
.getByRole("button", { name: /^filter$/i })
.first();
await expect(filterBtn).toBeVisible({ timeout: 5_000 });
await filterBtn.click();
const overlay = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog']",
);
await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
await expect(overlay.first().getByText(/front.?yard/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
test("Last 24 Hours calendar button opens date picker", async ({
test("Calendar trigger opens a date picker popover", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const review = new ReviewPage(frigateApp.page, true);
await expect(review.calendarTrigger).toBeVisible({ timeout: 5_000 });
await review.calendarTrigger.click();
const calendarBtn = frigateApp.page.getByRole("button", {
name: /24 hours|calendar|date/i,
});
if (await calendarBtn.isVisible().catch(() => false)) {
await calendarBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover should open
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
if (
await popover
.first()
.isVisible()
.catch(() => false)
) {
await frigateApp.page.keyboard.press("Escape");
}
}
// react-day-picker v9 renders a role="grid" calendar with day cells
// as buttons inside gridcells (e.g. "Wednesday, April 1st, 2026").
// The calendar is placed directly in the DOM (not always inside a
// Radix popper wrapper), so scope by the grid role instead.
const calendarGrid = frigateApp.page.locator('[role="grid"]').first();
await expect(calendarGrid).toBeVisible({ timeout: 3_000 });
const dayButton = calendarGrid.locator('[role="gridcell"] button').first();
await expect(dayButton).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape");
});
test("Filter button opens filter popover", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const filterBtn = frigateApp.page.getByRole("button", {
name: /^filter$/i,
});
if (await filterBtn.isVisible().catch(() => false)) {
await filterBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover or dialog should open
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog']",
);
if (
await popover
.first()
.isVisible()
.catch(() => false)
) {
await frigateApp.page.keyboard.press("Escape");
}
}
});
});
test.describe("Review Page - Timeline @critical", () => {
test("review page has timeline with time markers (desktop)", async ({
test("Show Reviewed switch flips its checked state", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
// "Show Reviewed" is a Radix Switch (role=switch), not a button.
// It filters review data client-side; it does not trigger a new
// /api/review network request. Verify the switch state toggles.
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(2000);
// Timeline renders time labels like "4:30 PM"
const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText).toMatch(/[AP]M/);
const showReviewedSwitch = frigateApp.page.getByRole("switch", {
name: /show reviewed/i,
});
await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 });
// Record initial checked state and click to toggle
const initialChecked =
await showReviewedSwitch.getAttribute("aria-checked");
await showReviewedSwitch.click();
const flippedChecked = initialChecked === "true" ? "false" : "true";
await expect(showReviewedSwitch).toHaveAttribute(
"aria-checked",
flippedChecked,
);
});
});
test.describe("Review Page - Navigation @critical", () => {
test("navigate to review from live page works", async ({ frigateApp }) => {
test.describe("Review — timeline (desktop) @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Timeline not shown on mobile",
);
test("timeline renders time markers", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await expect
.poll(
async () => (await frigateApp.page.textContent("#pageRoot")) ?? "",
{ timeout: 10_000 },
)
.toMatch(/[AP]M|\d+:\d+/);
});
});
test.describe("Review — mobile @critical @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("severity tabs render on mobile", async ({ frigateApp }) => {
await frigateApp.goto("/review");
const review = new ReviewPage(frigateApp.page, false);
await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
await expect(review.detectionsTab).toBeVisible();
});
test("back navigation returns to Live", async ({ frigateApp }) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
const base = new BasePage(frigateApp.page, false);
await base.navigateTo("/review");
await expect(frigateApp.page).toHaveURL(/\/review/);
// Severity tabs should be visible
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
await base.navigateTo("/");
await expect(frigateApp.page).toHaveURL(/\/$/);
});
});
+175 -28
View File
@@ -1,15 +1,21 @@
/**
* System page tests -- MEDIUM tier.
* System page tests -- MEDIUM tier (promoted to cover migrated
* RestartDialog test from radix-overlay-regressions.spec.ts).
*
* Tests system page rendering with tabs and tab switching.
* Navigates to /system#general explicitly so useHashState resolves
* the tab state deterministically.
* Tab switching, version + last-refreshed display, and the
* RestartDialog cancel flow.
*/
import { test, expect } from "../fixtures/frigate-test";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
test.describe("System Page @medium", () => {
test("system page renders with tab buttons", async ({ frigateApp }) => {
test.describe("System — tabs @medium", () => {
test("general tab is active by default via #general hash", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
@@ -20,7 +26,7 @@ test.describe("System Page @medium", () => {
await expect(frigateApp.page.getByLabel("Select cameras")).toBeVisible();
});
test("general tab is active when navigated via hash", async ({
test("Storage tab activates and deactivates General", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
@@ -29,18 +35,6 @@ test.describe("System Page @medium", () => {
"on",
{ timeout: 15_000 },
);
});
test("clicking Storage tab activates it and deactivates General", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select storage").click();
await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
"data-state",
@@ -53,29 +47,22 @@ test.describe("System Page @medium", () => {
);
});
test("clicking Cameras tab activates it and deactivates General", async ({
frigateApp,
}) => {
test("Cameras tab activates", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select cameras").click();
await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"off",
);
});
test("system page shows version and last refreshed", async ({
test("general tab shows version and last-refreshed", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
@@ -87,4 +74,164 @@ test.describe("System Page @medium", () => {
await expect(frigateApp.page.getByText("0.15.0-test")).toBeVisible();
await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible();
});
test("storage tab renders content after switching", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select storage").click();
await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
// On desktop, tab buttons render text labels so the word "storage"
// always appears in #pageRoot after switching. On mobile, tabs are
// icon-only, so we verify the general-tab content disappears instead
// (the storage tab's metrics section is hidden but general is gone).
if (!frigateApp.isMobile) {
await expect
.poll(
async () => (await frigateApp.page.textContent("#pageRoot")) ?? "",
{ timeout: 10_000 },
)
.toMatch(/storage|mount|disk|used|free/i);
} else {
// Mobile: tab activation (data-state "on") already asserted above.
// Additionally confirm general tab is no longer the active tab.
await expect(
frigateApp.page.getByLabel("Select general"),
).toHaveAttribute("data-state", "off", { timeout: 5_000 });
}
});
test("cameras tab renders each configured camera", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select cameras").click();
await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
// Cameras tab lists every camera from config/stats. The default
// mock has front_door, backyard, garage.
for (const cam of ["front_door", "backyard", "garage"]) {
await expect(
frigateApp.page
.getByText(new RegExp(cam.replace("_", ".?"), "i"))
.first(),
).toBeVisible({ timeout: 10_000 });
}
});
test("enrichments tab renders when semantic search is enabled", async ({
frigateApp,
}) => {
// Override config to guarantee the enrichments tab is present.
// System.tsx shows the tab when semantic_search.enabled === true.
await frigateApp.installDefaults({
config: { semantic_search: { enabled: true } },
});
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
const enrichTab = frigateApp.page.getByLabel(/select enrichments/i).first();
await expect(enrichTab).toBeVisible({ timeout: 5_000 });
await enrichTab.click();
await expect(enrichTab).toHaveAttribute("data-state", "on", {
timeout: 5_000,
});
});
});
test.describe("System — RestartDialog @medium", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Sidebar menu is desktop-only",
);
test("cancelling restart leaves body interactive", async ({ frigateApp }) => {
// Migrated from radix-overlay-regressions.spec.ts.
await frigateApp.goto("/");
const sidebarTriggers = frigateApp.page
.locator('[role="complementary"] [aria-haspopup="menu"]')
.or(frigateApp.page.locator('aside [aria-haspopup="menu"]'));
const triggerCount = await sidebarTriggers.count();
expect(triggerCount).toBeGreaterThan(0);
let opened = false;
for (let i = 0; i < triggerCount; i++) {
const trigger = sidebarTriggers.nth(i);
await trigger.click().catch(() => {});
const restartItem = frigateApp.page
.getByRole("menuitem", { name: /restart/i })
.first();
const visible = await expect(restartItem)
.toBeVisible({ timeout: 300 })
.then(() => true)
.catch(() => false);
if (visible) {
await restartItem.click();
opened = true;
break;
}
await frigateApp.page.keyboard.press("Escape").catch(() => {});
}
expect(opened).toBe(true);
const cancel = frigateApp.page.getByRole("button", { name: /cancel/i });
await expect(cancel).toBeVisible({ timeout: 3_000 });
await cancel.click();
await waitForBodyInteractive(frigateApp.page);
await expectBodyInteractive(frigateApp.page);
const postCancelTrigger = sidebarTriggers.first();
await postCancelTrigger.click();
await expect(
frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first(),
).toBeVisible({ timeout: 3_000 });
});
});
test.describe("System — mobile @medium @mobile", () => {
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
test("tabs render at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 15_000,
});
});
test("switching tabs works at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select storage").click();
await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
});
});
+487 -283
View File
@@ -12,14 +12,14 @@
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^3.10.0",
"@melloware/react-logviewer": "^6.1.2",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.8",
@@ -54,7 +54,7 @@
"immer": "^10.1.1",
"js-yaml": "^4.1.1",
"konva": "^10.2.3",
"lodash": "^4.17.23",
"lodash": "^4.18.1",
"lucide-react": "^0.577.0",
"monaco-yaml": "^5.4.1",
"next-themes": "^0.4.6",
@@ -1515,17 +1515,17 @@
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dialog": "1.1.6",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1542,126 +1542,17 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-scope": {
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -1672,6 +1563,29 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -2113,17 +2027,17 @@
}
},
"node_modules/@radix-ui/react-context-menu": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz",
"integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==",
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
"integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-menu": "2.1.6",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-menu": "2.1.16",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2140,6 +2054,99 @@
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
@@ -2197,21 +2204,6 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -2398,18 +2390,18 @@
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz",
"integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==",
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-menu": "2.1.6",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-menu": "2.1.16",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2426,10 +2418,106 @@
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -2505,20 +2593,20 @@
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz",
"integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2535,17 +2623,35 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2562,14 +2668,13 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2586,13 +2691,14 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2604,6 +2710,21 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-icons": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
@@ -2678,27 +2799,27 @@
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz",
"integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==",
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-roving-focus": "1.1.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
@@ -2717,17 +2838,22 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2744,15 +2870,62 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": {
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2769,14 +2942,13 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2793,14 +2965,76 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2869,21 +3103,6 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -3717,21 +3936,6 @@
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -7771,9 +7975,9 @@
"dev": true
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
@@ -9432,15 +9636,15 @@
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash.merge": {
+5 -5
View File
@@ -26,14 +26,14 @@
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^3.10.0",
"@melloware/react-logviewer": "^6.1.2",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.8",
@@ -68,7 +68,7 @@
"immer": "^10.1.1",
"js-yaml": "^4.1.1",
"konva": "^10.2.3",
"lodash": "^4.17.23",
"lodash": "^4.18.1",
"lucide-react": "^0.577.0",
"monaco-yaml": "^5.4.1",
"next-themes": "^0.4.6",
+2 -1
View File
@@ -6,7 +6,8 @@
"title": "يتم إعادة تشغيل فرايجيت",
"content": "العد التنازلي",
"button": "فرض إعادة التحميل الآن"
}
},
"description": "هذا سيؤدي لإيقاف Frigate مؤقتا أثناء إعادة تشغيلها"
},
"explore": {
"plus": {
+4 -1
View File
@@ -1,3 +1,6 @@
{
"label": "اعدادات الكاميرا"
"label": "اعدادات الكاميرا",
"name": {
"label": "إسم الكاميرا"
}
}
+6 -1
View File
@@ -1 +1,6 @@
{}
{
"version": {
"label": "إصدار الإعدادات الحالية",
"description": "نسحة عددية أو نصية من الإعدادات الحالية الفعالة للمساعدة على اكتشاف الانتقال أو التغير في الصِّيَغ"
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
{
"audio": {
"global": {
"detection": "التحري العام"
"detection": "التحري العام",
"sensitivity": "الحساسية العامة"
}
}
}
+4 -1
View File
@@ -1 +1,4 @@
{}
{
"minimum": "يجب أن تكون {{limit}} على الأقل",
"maximum": "يجب أن تكون {{limit}} كحد أقصى"
}
+3
View File
@@ -0,0 +1,3 @@
{
"documentTitle": "المحادثات - Frigate"
}
@@ -0,0 +1,3 @@
{
"documentTitle": "البحث عن الحركة - Frigate"
}
+1
View File
@@ -0,0 +1 @@
{}
+8 -1
View File
@@ -1 +1,8 @@
{}
{
"auth": {
"label": "Автентикация",
"session_length": {
"label": "Продължителност на сесията"
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+503
View File
@@ -0,0 +1,503 @@
{
"speech": "Govor",
"babbling": "Babavljanje",
"bicycle": "Kolo",
"yell": "Vik",
"bellow": "Bubanj",
"whoop": "Vrisak",
"whispering": "Šaputanje",
"laughter": "Smijeh",
"snicker": "Prijem",
"crying": "Plač",
"sigh": "Usklik",
"singing": "Pjevanje",
"choir": "Hors",
"yodeling": "Jodelanje",
"chant": "Pjevanje",
"mantra": "Mantra",
"child_singing": "Dječje pjevanje",
"synthetic_singing": "Sintetičko pjevanje",
"rapping": "Rap",
"humming": "Hum",
"groan": "Grokot",
"grunt": "Groktanje",
"whistling": "Pucanje",
"breathing": "Disanje",
"wheeze": "Pijuckanje",
"snoring": "Kicanje",
"gasp": "Udah",
"pant": "Pantanje",
"snort": "Snortanje",
"cough": "Kašljanje",
"throat_clearing": "Očišćavanje grla",
"sneeze": "Prašanje",
"sniff": "Njuhanje",
"run": "Trčanje",
"shuffle": "Prelazak",
"footsteps": "Koraci",
"chewing": "Zubljanje",
"biting": "Gubitak",
"gargling": "Peranje grla",
"stomach_rumble": "Grušenje",
"burping": "Puknutje",
"hiccup": "Kikot",
"fart": "Pucanje",
"hands": "Ruke",
"finger_snapping": "Prašanje prstiju",
"clapping": "Ključanje",
"heartbeat": "Taktilno",
"heart_murmur": "Šum srca",
"cheering": "Pozdrav",
"applause": "Pozdravljati",
"chatter": "Šaputanje",
"crowd": "Gomila",
"children_playing": "Dječja igra",
"animal": "Životinja",
"pets": "Hrana",
"dog": "Pas",
"bark": "Glavu",
"yip": "Jauk",
"howl": "Vijuk",
"bow_wow": "Vau vau",
"growling": "Gručenje",
"whimper_dog": "Pijuckanje psa",
"cat": "Mačka",
"purr": "Mrmor",
"meow": "Mjau",
"hiss": "Zujanje",
"caterwaul": "Krik",
"livestock": "Stoke",
"horse": "Konj",
"clip_clop": "Klik klok",
"neigh": "Kijanje",
"cattle": "Stoke",
"moo": "Muu",
"cowbell": "Kovčeg",
"pig": "Svinja",
"oink": "Roktanje",
"goat": "Koza",
"bleat": "Blejkanje",
"sheep": "Ovca",
"fowl": "Ptica",
"chicken": "Pilica",
"cluck": "Kukanje",
"cock_a_doodle_doo": "Kukavica",
"turkey": "Gusa",
"gobble": "Gubljanje",
"duck": "Kuja",
"quack": "Kvaka",
"goose": "Guska",
"honk": "Trubljenje",
"wild_animals": "Divlja životinja",
"roaring_cats": "Vrišćeći mački",
"roar": "Vrištanje",
"bird": "Ptica",
"chirp": "Pijuckanje",
"squawk": "Krik",
"pigeon": "Papiga",
"coo": "Kukanje",
"crow": "Vran",
"caw": "Vranje",
"owl": "Kukavica",
"hoot": "Kukavica",
"flapping_wings": "Mahanje krilima",
"dogs": "Psi",
"rats": "Štakori",
"mouse": "Miš",
"patter": "Topotanje",
"insect": "Insekt",
"cricket": "Cvrčak",
"mosquito": "Komarac",
"fly": "Muha",
"buzz": "Zujanje",
"frog": "Žaba",
"croak": "Kreketanje",
"snake": "Zmija",
"rattle": "Zveckanje",
"whale_vocalization": "Glasanje kita",
"music": "Muzika",
"musical_instrument": "Muzički instrument",
"plucked_string_instrument": "Plucked String Instrument",
"guitar": "Gitara",
"electric_guitar": "Električna gitara",
"bass_guitar": "Bas gitara",
"acoustic_guitar": "Akustična gitara",
"steel_guitar": "Steel gitara",
"tapping": "Tapping",
"strum": "Strum",
"banjo": "Bendžo",
"sitar": "Sitar",
"mandolin": "Mandolina",
"zither": "Citra",
"ukulele": "Ukulele",
"keyboard": "Klaviatura",
"piano": "Klavir",
"electric_piano": "Električni piano",
"organ": "Orgulje",
"electronic_organ": "Elektronski organ",
"hammond_organ": "Hammond organ",
"synthesizer": "Sintetizator",
"sampler": "Sampler",
"harpsichord": "Harfura",
"percussion": "Percuzija",
"drum_kit": "Set bubnjeva",
"drum_machine": "Mašina za bubnjeve",
"drum": "Bubanj",
"snare_drum": "Bubanj sa zavojima",
"rimshot": "Rimshot",
"drum_roll": "Bubanj za roliranje",
"bass_drum": "Bubanj za bas",
"timpani": "Timpani",
"tabla": "Tabla",
"cymbal": "Cimbale",
"hi_hat": "Hi-Hat",
"wood_block": "Drveni blok",
"tambourine": "Tamburina",
"maraca": "Maraka",
"gong": "Gong",
"tubular_bells": "Cijevasti zvoni",
"mallet_percussion": "Percusija s mljevima",
"marimba": "Marimba",
"glockenspiel": "Glockenspiel",
"vibraphone": "Vibrafon",
"steelpan": "Stelpan",
"orchestra": "Orkestar",
"brass_instrument": "Bronski instrument",
"french_horn": "Francuski rog",
"trumpet": "Truba",
"trombone": "Trombon",
"bowed_string_instrument": "Užadno strunski instrument",
"string_section": "Strunski sekcija",
"violin": "Violina",
"pizzicato": "Pizzicato",
"cello": "Celula",
"double_bass": "Dvostruki bas",
"wind_instrument": "Vjetreni instrument",
"flute": "Flauta",
"saxophone": "Saksafon",
"clarinet": "Klarinet",
"harp": "Harfa",
"bell": "Zvono",
"church_bell": "Crkveno zvono",
"jingle_bell": "Zvono za igračke",
"bicycle_bell": "Zvono za bicikl",
"tuning_fork": "Zvučnik",
"chime": "Zvono",
"wind_chime": "Vjetrenjac",
"harmonica": "Harmonika",
"accordion": "Akkordon",
"bagpipes": "Bogovina",
"didgeridoo": "Didgeridoo",
"theremin": "Teremin",
"singing_bowl": "Pjevni čaša",
"scratching": "Skrečing",
"pop_music": "Pop muzika",
"hip_hop_music": "Hip-Hop muzika",
"beatboxing": "Bitboksing",
"rock_music": "Rock muzika",
"heavy_metal": "Heavy metal",
"punk_rock": "Punk rock",
"grunge": "Grandž",
"progressive_rock": "Progressivni rock",
"rock_and_roll": "Rock and roll",
"psychedelic_rock": "Psihederički rock",
"rhythm_and_blues": "Ritam i blues",
"soul_music": "Soul glazba",
"reggae": "Rege",
"country": "Kantri",
"swing_music": "Swing glazba",
"bluegrass": "Bluegrass",
"funk": "Fank",
"folk_music": "Folklorno glazba",
"middle_eastern_music": "Glazba Bliskog istoka",
"jazz": "Džez",
"disco": "Disko",
"classical_music": "Klasična glazba",
"opera": "Opera",
"electronic_music": "Elektronska glazba",
"house_music": "House glazba",
"techno": "Tehno",
"dubstep": "Dubstep",
"drum_and_bass": "Drum i bass",
"electronica": "Elektronika",
"electronic_dance_music": "Elektronska plesna glazba",
"ambient_music": "Ambient glazba",
"trance_music": "Trance glazba",
"music_of_latin_america": "Glazba Latinske Amerike",
"salsa_music": "Salsa glazba",
"flamenco": "Flamenko",
"blues": "Bluz",
"music_for_children": "Muzika za djecu",
"new-age_music": "Muzika novog doba",
"vocal_music": "Vokalna muzika",
"a_capella": "A Capella",
"music_of_africa": "Afrička muzika",
"afrobeat": "Afrobeat",
"christian_music": "Kršćanska muzika",
"gospel_music": "Gospel muzika",
"music_of_asia": "Azijatska muzika",
"carnatic_music": "Karnatička muzika",
"music_of_bollywood": "Bollywood muzika",
"ska": "Ska",
"traditional_music": "Tradicionalna muzika",
"independent_music": "Nezavisna muzika",
"song": "Pjesma",
"background_music": "Pozadinska muzika",
"theme_music": "Tema muzika",
"jingle": "Jingle",
"soundtrack_music": "Soundtrack muzika",
"lullaby": "Pjesma za uspavanje",
"video_game_music": "Muzika za video igre",
"christmas_music": "Božićna muzika",
"dance_music": "Dance muzika",
"wedding_music": "Venčanska glazba",
"happy_music": "Sretna glazba",
"sad_music": "Tužna glazba",
"tender_music": "Tenderna glazba",
"exciting_music": "Uzbudljiva glazba",
"angry_music": "Zlobna glazba",
"scary_music": "Strašna glazba",
"wind": "Vjetar",
"rustling_leaves": "Šum listova",
"wind_noise": "Šum vjetra",
"thunderstorm": "Grmljavina",
"thunder": "Grmljavac",
"water": "Voda",
"rain": "Kisa",
"raindrop": "Kap kise",
"rain_on_surface": "Kisa na površini",
"stream": "Tok",
"waterfall": "Padina",
"ocean": "Okean",
"waves": "Valovi",
"steam": "Par",
"gurgling": "Gurkanje",
"fire": "Vatra",
"crackle": "Krik",
"vehicle": "Vozilo",
"boat": "Brod",
"sailboat": "Jedrilica",
"rowboat": "Čamac",
"motorboat": "Motorni čamac",
"ship": "Brod",
"motor_vehicle": "Motorno vozilo",
"car": "Automobil",
"toot": "Zvuk klaksona",
"car_alarm": "Automobilski alarm",
"power_windows": "Električna prozora",
"skidding": "Klizanje",
"tire_squeal": "Krik kotača",
"car_passing_by": "Automobil prolazi",
"race_car": "Racing automobil",
"truck": "Kamion",
"air_brake": "Vazdušni kočnici",
"air_horn": "Vazdušni signal",
"reversing_beeps": "Zvukovi za odlazak unazad",
"ice_cream_truck": "Kamion za sladoled",
"bus": "Autobus",
"emergency_vehicle": "Hitni vozilo",
"police_car": "Policijski automobil",
"ambulance": "Ambulansa",
"fire_engine": "Pogonski automobil",
"motorcycle": "Motocikl",
"traffic_noise": "Prometni šum",
"rail_transport": "Željeznički transport",
"train": "Vlak",
"train_whistle": "Vlakovni svirac",
"train_horn": "Vlakovni rohorn",
"railroad_car": "Željeznički vagon",
"train_wheels_squealing": "Vlakove točkove koje zavijaju",
"subway": "Metropolitena",
"aircraft": "Avion",
"aircraft_engine": "Avionski motor",
"jet_engine": "Reaktivni motor",
"propeller": "Vijak",
"helicopter": "Heličopter",
"fixed-wing_aircraft": "Avion s krilima",
"skateboard": "Skejtbord",
"engine": "Motor",
"light_engine": "Lagani motor",
"dental_drill's_drill": "Stomatološki bušilica",
"lawn_mower": "Kosilica",
"chainsaw": "Pilica",
"medium_engine": "Srednji motor",
"heavy_engine": "Teški motor",
"engine_knocking": "Kloping motora",
"engine_starting": "Pokretanje motora",
"idling": "Miris",
"accelerating": "Ubrzavanje",
"door": "Vrata",
"doorbell": "Zvonce",
"ding-dong": "Ding-dong",
"sliding_door": "Klizna vrata",
"slam": "Zatvaranje",
"knock": "Kucanje",
"tap": "Kucanje",
"squeak": "Krik",
"cupboard_open_or_close": "Otvorenje ili zatvaranje police",
"drawer_open_or_close": "Otvorenje ili zatvaranje vunca",
"dishes": "Posuđe",
"cutlery": "Posuđe za jelo",
"chopping": "Rezanje",
"frying": "Praženje",
"microwave_oven": "Mikrotalasna pećnica",
"blender": "Miksere",
"water_tap": "Kran",
"sink": "Lavabo",
"bathtub": "Kupatilo",
"hair_dryer": "Sušilac za kosu",
"toilet_flush": "Očišćavanje toaleta",
"toothbrush": "Šetka za zube",
"electric_toothbrush": "Električna šetka za zube",
"vacuum_cleaner": "Praškoljac",
"zipper": "Zatvarac",
"keys_jangling": "Ključevi koji se škripi",
"coin": "Novčanik",
"scissors": "Škare",
"electric_shaver": "Električni šavac",
"shuffling_cards": "Premještanje karata",
"typing": "Kucanje",
"typewriter": "Tipkovnica",
"computer_keyboard": "Računalna tipkovnica",
"writing": "Pisanje",
"alarm": "Alarm",
"telephone": "Telefon",
"telephone_bell_ringing": "Zvono telefona",
"ringtone": "Ton za poziv",
"telephone_dialing": "Pozivanje telefona",
"dial_tone": "Ton za poziv",
"busy_signal": "Signal zauzetosti",
"alarm_clock": "Budilica",
"siren": "Sirena",
"civil_defense_siren": "Sirena za civilnu zaštitu",
"buzzer": "Buzer",
"smoke_detector": "Detektor dima",
"fire_alarm": "Pozar alarm",
"foghorn": "Mlazni svirac",
"whistle": "Štiklja",
"steam_whistle": "Parni zvono",
"mechanisms": "Mehanizmi",
"ratchet": "Ratchet",
"clock": "Sat",
"tick": "Tik",
"tick-tock": "Tik-tak",
"gears": "Zupčanici",
"pulleys": "Koturači",
"sewing_machine": "Šitna mašina",
"mechanical_fan": "Mehanički ventilator",
"air_conditioning": "Klima uređaj",
"cash_register": "Gotovinska kasica",
"printer": "Štampač",
"camera": "Kamera",
"single-lens_reflex_camera": "Kamera s jednim objektivom",
"tools": "Alati",
"hammer": "Klubica",
"jackhammer": "Betonomijak",
"sawing": "Sečenje",
"filing": "Flešanje",
"sanding": "Šljokanje",
"power_tool": "Električni alat",
"drill": "Bušilica",
"explosion": "Eksplozija",
"gunshot": "Pucanj",
"machine_gun": "Automatska puška",
"fusillade": "Fusiladža",
"artillery_fire": "Pucanj topovima",
"cap_gun": "Pistolj za pucanje",
"fireworks": "Pucanje svjetiljki",
"firecracker": "Svjetiljka",
"burst": "Izbič",
"eruption": "Eruptija",
"boom": "Tutnjava",
"wood": "Drvo",
"chop": "Rezanje",
"splinter": "Razlomak",
"crack": "Klackanje",
"glass": "Staklo",
"chink": "Prozor",
"shatter": "Razbijanje",
"silence": "Tišina",
"sound_effect": "Zvučni efekt",
"environmental_noise": "Okolišni šum",
"static": "Statički šum",
"white_noise": "Bijeli šum",
"pink_noise": "Rumeni šum",
"television": "Televizija",
"radio": "Radio",
"field_recording": "Snimka na terenu",
"scream": "Vrisak",
"sodeling": "Sodeling",
"chird": "Chird",
"change_ringing": "Promjena zvona",
"shofar": "Šofar",
"liquid": "Tekućina",
"splash": "Pljuskanje",
"slosh": "Sloš",
"squish": "Škripanje",
"drip": "Kapanje",
"pour": "Prelivanje",
"trickle": "Tijek",
"gush": "Gusenje",
"fill": "Popunjavanje",
"spray": "Sprajanje",
"pump": "Pumpa",
"stir": "Miješanje",
"boiling": "Vrećenje",
"sonar": "Sonar",
"arrow": "Strela",
"whoosh": "Šum",
"thump": "Tupanje",
"thunk": "Tunk",
"electronic_tuner": "Elektronski tuner",
"effects_unit": "Jedinica efekata",
"chorus_effect": "Efekt korusa",
"basketball_bounce": "Košarkaški skok",
"bang": "Bum",
"slap": "Pljeska",
"whack": "Perc",
"smash": "Sprem",
"breaking": "Raskidanje",
"bouncing": "Skakanje",
"whip": "Škripanje",
"flap": "Klizanje",
"scratch": "Oštećenje",
"scrape": "Prašenje",
"rub": "Trenje",
"roll": "Kotrljanje",
"crushing": "Stiskanje",
"crumpling": "Sklapanje",
"tearing": "Raskidanje",
"beep": "Bip",
"ping": "Poziv",
"ding": "Ding",
"clang": "Zveket",
"squeal": "Cika",
"creak": "Škripa",
"rustle": "Šuškanje",
"whir": "Brujanje",
"clatter": "Tropot",
"sizzle": "Šištanje",
"clicking": "Klikanje",
"clickety_clack": "Klik-tak",
"rumble": "Rumbljanje",
"plop": "Pljus",
"hum": "Pjevušenje",
"zing": "Zing",
"boing": "Boing",
"crunch": "Crunch",
"sine_wave": "Sinusna valna",
"harmonic": "Harmonični",
"chirp_tone": "Tanjirasti ton",
"pulse": "Impuls",
"inside": "Unutra",
"outside": "Van",
"reverberation": "Reverberacija",
"echo": "Odjek",
"noise": "Šum",
"mains_hum": "Glavni šum",
"distortion": "Distorzija",
"sidetone": "Sidetone",
"cacophony": "Kacofonija",
"throbbing": "Tremor",
"vibration": "Vibracija"
}
+326
View File
@@ -0,0 +1,326 @@
{
"time": {
"untilForTime": "Do {{time}}",
"untilForRestart": "Do ponovnog pokretanja Frigate.",
"untilRestart": "Do ponovnog pokretanja",
"never": "Nikad",
"ago": "{{timeAgo}} prije",
"justNow": "Sada",
"today": "Danas",
"yesterday": "Jučer",
"last7": "Prošlih 7 dana",
"last14": "Prošlih 14 dana",
"last30": "Prošlih 30 dana",
"thisWeek": "Ova sedmica",
"lastWeek": "Prošla sedmica",
"thisMonth": "Ovaj mjesec",
"lastMonth": "Prošli mjesec",
"5minutes": "5 minuta",
"10minutes": "10 minuta",
"30minutes": "30 minuta",
"1hour": "1 sat",
"12hours": "12 sati",
"24hours": "24 sata",
"pm": "posle podne",
"am": "pre podne",
"yr": "{{time}} god",
"year_one": "{{time}} godina",
"year_few": "{{time}} godine",
"year_other": "{{time}} godina",
"mo": "{{time}} mjes",
"month_one": "{{time}} mjesec",
"month_few": "{{time}} mjeseca",
"month_other": "{{time}} mjeseci",
"d": "{{time}}d",
"day_one": "{{time}} dan",
"day_few": "{{time}} dana",
"day_other": "{{time}} dana",
"h": "{{time}}h",
"hour_one": "{{time}} sat",
"hour_few": "{{time}} sata",
"hour_other": "{{time}} sati",
"m": "{{time}}m",
"minute_one": "{{time}} minuta",
"minute_few": "{{time}} minute",
"minute_other": "{{time}} minuta",
"s": "{{time}}s",
"second_one": "{{time}} sekunda",
"second_few": "{{time}} sekunde",
"second_other": "{{time}} sekundi",
"formattedTimestamp": {
"12hour": "MMM d, h:mm:ss aaa",
"24hour": "MMM d, HH:mm:ss"
},
"formattedTimestamp2": {
"12hour": "MM/dd h:mm:ssa",
"24hour": "d MMM HH:mm:ss"
},
"formattedTimestampHourMinute": {
"12hour": "h:mm aaa",
"24hour": "HH:mm"
},
"formattedTimestampHourMinuteSecond": {
"12hour": "h:mm:ss aaa",
"24hour": "HH:mm:ss"
},
"formattedTimestampMonthDayHourMinute": {
"12hour": "MMM d, h:mm aaa",
"24hour": "MMM d, HH:mm"
},
"formattedTimestampMonthDayYear": {
"12hour": "MMM d, yyyy",
"24hour": "MMM d, yyyy"
},
"formattedTimestampMonthDayYearHourMinute": {
"12hour": "MMM d yyyy, h:mm aaa",
"24hour": "MMM d yyyy, HH:mm"
},
"formattedTimestampMonthDay": "MMM d",
"formattedTimestampFilename": {
"12hour": "MM-dd-yy-h-mm-ss-a",
"24hour": "MM-dd-yy-HH-mm-ss"
},
"inProgress": "U toku",
"invalidStartTime": "Neispravno početno vrijeme",
"invalidEndTime": "Neispravno krajnje vrijeme"
},
"unit": {
"speed": {
"mph": "mph",
"kph": "kph"
},
"length": {
"feet": "fut",
"meters": "metar"
},
"data": {
"kbps": "kB/s",
"mbps": "MB/s",
"gbps": "GB/s",
"kbph": "kB/hour",
"mbph": "MB/hour",
"gbph": "GB/hour"
}
},
"label": {
"back": "Povratak",
"hide": "Sakrij {{item}}",
"show": "Prikaži {{item}}",
"ID": "ID",
"none": "Nijedan",
"all": "Sve",
"other": "Ostalo"
},
"list": {
"two": "{{0}} i {{1}}",
"many": "{{items}}, i {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Opcionalno",
"internalID": "Unutarnji ID koji Frigate koristi u konfiguraciji i bazi podataka"
},
"button": {
"add": "Dodaj",
"apply": "Primijeni",
"applying": "Primjenjuje se…",
"reset": "Resetuj",
"undo": "Poništi",
"done": "Gotovo",
"enabled": "Omogućeno",
"enable": "Omogući",
"disabled": "Onemogućeno",
"disable": "Onemogući",
"save": "Sačuvaj",
"saving": "Sačuvanje…",
"cancel": "Otkaži",
"close": "Zatvori",
"copy": "Kopiraj",
"copiedToClipboard": "Kopirano u međuspremnik",
"back": "Nazad",
"history": "Historija",
"fullscreen": "Pun ekran",
"exitFullscreen": "Napusti pun ekran",
"pictureInPicture": "Slika u slici",
"twoWayTalk": "Dvostrani razgovor",
"cameraAudio": "Zvuk kamere",
"on": "Uključeno",
"off": "Isključeno",
"edit": "Uredi",
"copyCoordinates": "Kopiraj koordinate",
"delete": "Obriši",
"yes": "Da",
"no": "Ne",
"download": "Preuzmi",
"info": "Informacija",
"suspended": "Otkazano",
"unsuspended": "Ponovi",
"play": "Reproduciraj",
"unselect": "Odznači",
"export": "Izvoz",
"deleteNow": "Obriši sada",
"next": "Sljedeće",
"continue": "Nastavi",
"modified": "Izmijenjeno",
"overridden": "Preklopljeno",
"resetToGlobal": "Vrati na globalno",
"resetToDefault": "Vrati na podrazumijevano",
"saveAll": "Sačuvaj sve",
"savingAll": "Sačuvanje svih…",
"undoAll": "Poništi sve",
"retry": "Pokušaj ponovno"
},
"menu": {
"system": "Sistem",
"systemMetrics": "Sistem metrike",
"configuration": "Konfiguracija",
"systemLogs": "Sistemski zapisi",
"profiles": "Profili",
"settings": "Postavke",
"configurationEditor": "Uređivač konfiguracije",
"languages": "Jezici",
"language": {
"en": "Engleski (English)",
"es": "Španjolski (Spanish)",
"zhCN": "Jednostavni kineski (Simplified Chinese)",
"hi": "Hindi (Hindi)",
"fr": "Francuski (French)",
"ar": "Arapski (Arabic)",
"pt": "Portugalski (Portuguese)",
"ptBR": "Portugalski brazilski (Brazilian Portuguese)",
"ru": "Ruski (Russian)",
"de": "Nemački (German)",
"ja": "Japanski (Japanese)",
"tr": "Turski (Turkish)",
"it": "Talijanski (Italian)",
"nl": "Nizozemski (Dutch)",
"sv": "Švedski (Swedish)",
"cs": "Češki (Czech)",
"nb": "Norveški bokmål (Norwegian Bokmål)",
"ko": "Koreanski (Korean)",
"vi": "Vietnamski (Vietnamese)",
"fa": "Perzijski (Persian)",
"pl": "Polski (Poljski)",
"uk": "Українська (Ukrajinski)",
"he": "עברית (Hebrejski)",
"el": "Ελληνικά (Grčki)",
"ro": "Română (Romunski)",
"hu": "Magyar (Mađarski)",
"fi": "Suomi (Finski)",
"da": "Dansk (Danski)",
"sk": "Slovenčina (Slovački)",
"yue": "粵語 (Kantonski)",
"th": "ไทย (Tajski)",
"ca": "Català (Katalonski)",
"hr": "Hrvatski (Hrvatski)",
"sr": "Српски (Srpski)",
"sl": "Slovenščina (Slovenski)",
"lt": "Lietuvių (Lietuvių)",
"bg": "Български (Bugarinski)",
"gl": "Galego (Galicijski)",
"id": "Bahasa Indonesia (Indoneziski)",
"ur": "اردو (Urdu)",
"withSystem": {
"label": "Koristite postavke sistema za jezik"
}
},
"appearance": "Izgled",
"darkMode": {
"label": "Tamni režim",
"light": "Svijetla",
"dark": "Tamna",
"withSystem": {
"label": "Koristite postavke sistema za svjetlosni ili tamni režim"
}
},
"withSystem": "Sistem",
"theme": {
"label": "Tema",
"blue": "Plava",
"green": "Zelena",
"nord": "Nord",
"red": "Crvena",
"highcontrast": "Visok kontrast",
"default": "Zadano"
},
"help": "Pomoć",
"documentation": {
"title": "Dokumentacija",
"label": "Dokumentacija za Frigate"
},
"restart": "Ponovno pokreni Frigate",
"live": {
"title": "Uživo",
"allCameras": "Sve Kamere",
"cameras": {
"title": "Kamere",
"count_one": "{{count}} Kamera",
"count_few": "{{count}} Kamere",
"count_other": "{{count}} Kamere"
}
},
"review": "Pregled",
"explore": "Istraži",
"export": "Izvoz",
"actions": "Akcije",
"uiPlayground": "UI Playground",
"features": "Funkcije",
"faceLibrary": "Biblioteka lica",
"classification": "Klasifikacija",
"chat": "Razgovor",
"user": {
"title": "Korisnik",
"account": "Račun",
"current": "Trenutni korisnik: {{user}}",
"anonymous": "anons",
"logout": "Odjava",
"setPassword": "Postavi lozinku"
}
},
"toast": {
"copyUrlToClipboard": "URL kopiran u međuspremnik.",
"save": {
"title": "Sačuvaj",
"error": {
"title": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}",
"noMessage": "Nije uspješno sačuvana promjena konfiguracije"
},
"success": "Uspješno sačuvana promjena konfiguracije."
}
},
"role": {
"title": "Uloga",
"admin": "Administrator",
"viewer": "Pregledač",
"desc": "Admini imaju pun pristup svim funkcijama u korisničkom sučelju Frigate. Pregledači su ograničeni na pregled kamere, pregled stavki i povijesne snimke u korisničkom sučelju."
},
"pagination": {
"label": "paginacija",
"previous": {
"title": "Prethodno",
"label": "Idi na prethodnu stranicu"
},
"next": {
"title": "Sljedeće",
"label": "Idi na sljedeću stranicu"
},
"more": "Više stranica"
},
"accessDenied": {
"documentTitle": "Pristup odbijen - Frigate",
"title": "Pristup odbijen",
"desc": "Nemate dozvolu za pregled ove stranice."
},
"notFound": {
"documentTitle": "Nije pronađeno - Frigate",
"title": "404",
"desc": "Stranica nije pronađena"
},
"selectItem": "Odaberite {{item}}",
"readTheDocumentation": "Pročitajte dokumentaciju",
"information": {
"pixels": "{{area}}px"
},
"no_items": "Nema stavki",
"validation_errors": "Greške validacije"
}
@@ -0,0 +1,16 @@
{
"form": {
"user": "Korisničko ime",
"password": "Lozinka",
"login": "Prijava",
"firstTimeLogin": "Pokušavate se prijaviti prvi put? Vjerodajnice su ispisane u logovima Frigate.",
"errors": {
"usernameRequired": "Korisničko ime je obavezno",
"passwordRequired": "Lozinka je obavezna",
"rateLimit": "Premašen je limit brzine. Pokušajte kasnije.",
"loginFailed": "Prijava nije uspješna",
"unknownError": "Nepoznata greška. Provjerite zapise.",
"webUnknownError": "Nepoznata greška. Provjerite konzolne zapise."
}
}
}
@@ -0,0 +1,87 @@
{
"group": {
"label": "Grupe kamere",
"add": "Dodaj grupu kamere",
"edit": "Uredi grupu kamera",
"delete": {
"label": "Obriši grupu kamere",
"confirm": {
"title": "Potvrdi brisanje",
"desc": "Sigurno li želite da obrišete grupu kamere <em>{{name}}</em>?"
}
},
"name": {
"label": "Ime",
"placeholder": "Unesite ime…",
"errorMessage": {
"mustLeastCharacters": "Ime grupe kamere mora imati najmanje 2 karaktera.",
"exists": "Ime grupe kamere već postoji.",
"nameMustNotPeriod": "Ime grupe kamere ne smije sadržavati tačku.",
"invalid": "Neispravno ime grupe kamere."
}
},
"cameras": {
"label": "Kamere",
"desc": "Odaberite kamere za ovu grupu."
},
"icon": "Ikona",
"success": "Grupa kamere ({{name}}) je sačuvana.",
"camera": {
"birdseye": "Birdseye",
"setting": {
"label": "Postavke prenošenja kamere",
"title": "Postavke prenošenja {{cameraName}}",
"desc": "Promijenite opcije uživo prenošenja za tablicu upravljanja ove grupe kamere. <em>Ove postavke su specifične za uređaj/pretvarač.</em>",
"audioIsAvailable": "Audio je dostupan za ovaj stream",
"audioIsUnavailable": "Zvuk nije dostupan za ovaj tok",
"audio": {
"tips": {
"title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
}
},
"stream": "Tok",
"placeholder": "Odaberite tok",
"streamMethod": {
"label": "Način prenošenja",
"placeholder": "Odaberite način prenošenja",
"method": {
"noStreaming": {
"label": "Bez prenošenja",
"desc": "Slike kamere će se ažurirati samo jednom na minut i neće se dogoditi uživo prenošenje."
},
"smartStreaming": {
"label": "Pametno prenošenje (preporučeno)",
"desc": "Pametno prenošenje će ažurirati sliku kamere jednom na minut kada se ne događa detektovana aktivnost kako bi se uštedjelo na širovini i resursima. Kada se detektuje aktivnost, slika se glatko prebacuje u uživo prenošenje."
},
"continuousStreaming": {
"label": "Neprekidno prenošenje",
"desc": {
"title": "Slika kamere uvijek će biti živo prenošenje kada je vidljiva na ploči, čak i ako se ne detektira aktivnost.",
"warning": "Neprekidno prenošenje može uzrokovati visoku upotrebu širine pojasa i probleme s performansama. Koristite s oprezom."
}
}
}
},
"compatibilityMode": {
"label": "Režim kompatibilnosti",
"desc": "Omogućite ovu opciju samo ako se živo prenošenje vaše kamere prikazuje s bojnim artefaktima i dijagonalnom linijom na desnoj strani slike."
}
}
}
},
"debug": {
"options": {
"label": "Postavke",
"title": "Opcije",
"showOptions": "Prikaži opcije",
"hideOptions": "Sakrij opcije"
},
"boundingBox": "Okvir",
"timestamp": "Vremenski pečat",
"zones": "Zone",
"mask": "Maska",
"motion": "Kretanje",
"regions": "Regije",
"paths": "Putanje"
}
}
@@ -0,0 +1,197 @@
{
"restart": {
"title": "Sigurni li ste da želite ponovno pokrenuti Frigate?",
"description": "Ovo privremeno zaustavi Frigate dok se ponovno pokreće.",
"button": "Ponovno pokretanje",
"restarting": {
"title": "Frigate se ponovo pokreće",
"content": "Ova stranica će se ponovno učitati za {{countdown}} sekundi.",
"button": "Silovito ponovno učitavanje sada"
}
},
"explore": {
"plus": {
"submitToPlus": {
"label": "Pošalji na Frigate+",
"desc": "Predmeti u lokacijama koje želite izbjeći nisu lažni pozitivi. Pošiljanje ih kao lažne pozitive zbunjuje model."
},
"review": {
"question": {
"label": "Potvrdite ovu oznaku za Frigate Plus",
"ask_a": "Je li ovaj objekt <code>{{label}}</code>?",
"ask_an": "Je li ovaj objekt <code>{{label}}</code>?",
"ask_full": "Je li ovaj objekt <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
},
"state": {
"submitted": "Pošlato"
}
}
},
"video": {
"viewInHistory": "Pregledajte u povijesti"
}
},
"export": {
"time": {
"fromTimeline": "Odaberite iz vremenske linije",
"lastHour_one": "Prošli sat",
"lastHour_few": "Prošla {{count}} sata",
"lastHour_other": "Prošlih {{count}} sati",
"custom": "Prilagođeno",
"start": {
"title": "Vrijeme početka",
"label": "Odaberite vrijeme početka"
},
"end": {
"title": "Vrijeme kraja",
"label": "Odaberite vrijeme kraja"
}
},
"name": {
"placeholder": "Nazovite izvoz"
},
"case": {
"newCaseOption": "Napravite novi slučaj",
"newCaseNamePlaceholder": "Novo ime slučaja",
"newCaseDescriptionPlaceholder": "Opis slučaja",
"label": "Slučaj",
"nonAdminHelp": "Za ove izvoze će se stvoriti novi slučaj.",
"placeholder": "Odaberite slučaj"
},
"select": "Odaberite",
"export": "Izvoz",
"queueing": "Stavljanje izvoza u red...",
"selectOrExport": "Odaberite ili izvozite",
"tabs": {
"export": "Jedna kamera",
"multiCamera": "Više kamera"
},
"multiCamera": {
"timeRange": "Vremenski opseg",
"selectFromTimeline": "Odaberite iz vremenske linije",
"cameraSelection": "Kamere",
"cameraSelectionHelp": "Kamere s praćenim objektima u ovom vremenskom opsegu su preselektirane",
"checkingActivity": "Provjeravamo aktivnost kamere...",
"noCameras": "Nema dostupnih kamera",
"detectionCount_one": "1 praćen objekt",
"detectionCount_few": "{{count}} praćena objekta",
"detectionCount_other": "{{count}} praćenih objekata",
"nameLabel": "Ime izvoza",
"namePlaceholder": "Nepovlačenje baznog imena za ove izvoze",
"queueingButton": "Stavljanje izvoza u red...",
"exportButton_one": "Izvoz 1 kamere",
"exportButton_few": "Izvoz {{count}} kamere",
"exportButton_other": "Izvoz {{count}} kamera"
},
"multi": {
"title_one": "Izvoz 1 pregleda",
"title_few": "Izvoz {{count}} pregleda",
"title_other": "Izvoz {{count}} pregleda",
"description": "Izvoz svakog odabranih pregleda. Svi izvozi bit će grupirani pod jedan slučaj.",
"descriptionNoCase": "Izvoz svakog odabranih pregleda.",
"caseNamePlaceholder": "Pregled izvoza - {{date}}",
"exportButton_one": "Izvoz 1 pregleda",
"exportButton_few": "Izvoz {{count}} pregleda",
"exportButton_other": "Izvoz {{count}} pregleda",
"exportingButton": "Izvoz...",
"toast": {
"started_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
"started_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
"started_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
"startedNoCase_one": "Pokrenut 1 izvoz.",
"startedNoCase_few": "Pokrenuta {{count}} izvoza.",
"startedNoCase_other": "Pokrenuto {{count}} izvoza.",
"partial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješno: {{failedItems}}",
"failed": "Neuspješno pokretanje {{total}} izvoza. Neuspješno: {{failedItems}}"
}
},
"toast": {
"success": "Uspješno pokrenut izvoz. Pregledajte datoteku na stranici izvoza.",
"queued": "Izvoz u redu. Pregledajte napredak na stranici izvoza.",
"view": "Pregled",
"batchSuccess_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
"batchSuccess_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
"batchSuccess_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
"batchPartial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchFailed": "Neuspješno pokretanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchQueuedSuccess_one": "U red stavljen 1 izvoz. Otvaranje slučaja sada.",
"batchQueuedSuccess_few": "U red stavljena {{count}} izvoza. Otvaranje slučaja sada.",
"batchQueuedSuccess_other": "U red stavljeno {{count}} izvoza. Otvaranje slučaja sada.",
"batchQueuedPartial": "U redu {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"batchQueueFailed": "Neuspješno dodavanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
"error": {
"failed": "Neuspješno dodavanje izvoza: {{error}}",
"endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena",
"noVaildTimeSelected": "Nije odabran valjan vremenski opseg"
}
},
"fromTimeline": {
"saveExport": "Sačuvaj izvoz",
"queueingExport": "Kopiranje izvoza...",
"previewExport": "Pregled izvoza",
"useThisRange": "Koristi ovaj opseg"
}
},
"streaming": {
"label": "Tok",
"restreaming": {
"disabled": "Restreaming nije omogućeno za ovu kameru.",
"desc": {
"title": "Postavite go2rtc za dodatne opcije uživog pregleda i zvuk za ovu kameru."
}
},
"showStats": {
"label": "Prikaži statistiku strima",
"desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
},
"debugView": "Pregled za otklanjanje grešaka"
},
"search": {
"saveSearch": {
"label": "Sačuvaj pretragu",
"desc": "Navedite ime za ovu sačuvanu pretragu.",
"placeholder": "Unesite ime za svoju pretragu",
"overwrite": "{{searchName}} već postoji. Sačuvavanje će prebrisati postojet će vrijednost.",
"success": "Pretraga ({{searchName}}) je sačuvana.",
"button": {
"save": {
"label": "Sačuvaj ovu pretragu"
}
}
}
},
"recording": {
"shareTimestamp": {
"label": "Dijeli vremensku oznaku",
"title": "Dijeli vremensku oznaku",
"description": "Dijelite URL označen vremenom trenutne pozicije igrača ili odaberite prilagođenu vremensku oznaku. Napomena: ovo nije javni URL za dijeljenje i dostupan je samo korisnicima koji imaju pristup Frigate i ovoj kameri.",
"custom": "Prilagođena vremenska oznaka",
"button": "URL za dijeljenje vremenske oznake",
"shareTitle": "Vremenska oznaka pregleda Frigate: {{camera}}"
},
"confirmDelete": {
"title": "Potvrdi brisanje",
"desc": {
"selected": "Sigurni li ste da želite izbrisati sve snimljeno video povezano s ovim preglednim stavkom?<br /><br />Zadržite tipku <em>Shift</em> da biste preskočili ovaj dijalog u budućnosti."
},
"toast": {
"success": "Video snimke povezane s odabranim preglednim stavcima uspješno su izbrisane.",
"error": "Neuspješno brisanje: {{error}}"
}
},
"button": {
"export": "Izvoz",
"markAsReviewed": "Označi kao pregledano",
"markAsUnreviewed": "Označi kao nepregledano",
"deleteNow": "Obriši sada"
}
},
"imagePicker": {
"selectImage": "Odaberite minijaturu praćenog objekta",
"unknownLabel": "Sačuvana slika izazivača",
"search": {
"placeholder": "Pretraga po oznaci ili podoznaci..."
},
"noImages": "Nema mini prikaza za ovu kameru"
}
}
@@ -0,0 +1,140 @@
{
"filter": "Filtar",
"classes": {
"label": "Klase",
"all": {
"title": "Sve klase"
},
"count_one": "{{count}} Klasa",
"count_other": "{{count}} Klase"
},
"labels": {
"label": "Oznake",
"all": {
"title": "Sve oznake",
"short": "Oznake"
},
"count_one": "{{count}} Oznaka",
"count_other": "{{count}} Oznake"
},
"zones": {
"label": "Zone",
"all": {
"title": "Sve zone",
"short": "Zone"
}
},
"dates": {
"selectPreset": "Odaberite predpostavku…",
"all": {
"title": "Svi datumi",
"short": "Datumi"
}
},
"more": "Više filtera",
"reset": {
"label": "Poništi filtere na zadane vrijednosti"
},
"timeRange": "Vremenski opseg",
"subLabels": {
"label": "Podoznake",
"all": "Sve podoznake"
},
"attributes": {
"label": "Atributi klasifikacije",
"all": "Svi atributi"
},
"score": "Rezultat",
"estimatedSpeed": "Procijenjena brzina ({{unit}})",
"features": {
"label": "Funkcije",
"hasSnapshot": "Ima snimak",
"hasVideoClip": "Ima video zapis",
"submittedToFrigatePlus": {
"label": "Predano Frigate+",
"tips": "Prvo morate filtrirati prateće objekte koji imaju snimak.<br /><br />Prateći objekti bez snimka ne mogu se poslati na Frigate+."
}
},
"sort": {
"label": "Sortiraj",
"dateAsc": "Datum (Uzlazno)",
"dateDesc": "Datum (Silazno)",
"scoreAsc": "Ocjena objekta (Uzlazno)",
"scoreDesc": "Ocjena objekta (Silazno)",
"speedAsc": "Procijenjena brzina (Uzlazno)",
"speedDesc": "Procijenjena brzina (Silazno)",
"relevance": "Relevantnost"
},
"cameras": {
"label": "Filter kamere",
"all": {
"title": "Sve Kamere",
"short": "Kamere"
}
},
"review": {
"showReviewed": "Prikaži pregledane"
},
"motion": {
"showMotionOnly": "Prikaži samo pokret"
},
"explore": {
"settings": {
"title": "Postavke",
"defaultView": {
"title": "Zadani prikaz",
"desc": "Kada nisu odabrani filteri, prikazuje se sažetak najnovijih pratećih objekata po oznaci, ili prikazuje se mreža bez filtriranja.",
"summary": "Sažetak",
"unfilteredGrid": "Mreža bez filtriranja"
},
"gridColumns": {
"title": "Kolone mreže",
"desc": "Odaberite broj kolona u prikazu mreže."
},
"searchSource": {
"label": "Izvor pretrage",
"desc": "Odaberite da li ćete pretraživati miniaturne slike ili opise vaših praćenih objekata.",
"options": {
"thumbnailImage": "Miniaturna slika",
"description": "Opis"
}
}
},
"date": {
"selectDateBy": {
"label": "Odaberite datum za filtriranje"
}
}
},
"logSettings": {
"label": "Filtrirajte nivo zapisa",
"filterBySeverity": "Filtrirajte zapise prema ozbiljnosti",
"loading": {
"title": "Učitavanje",
"desc": "Kada se panel zapisa pomakne do dna, novi zapisi automatski se prikazuju kada se dodaju."
},
"disableLogStreaming": "Onemogući praćenje zapisa",
"allLogs": "Svi zapisi"
},
"trackedObjectDelete": {
"title": "Potvrdi brisanje",
"desc": "Brisanje ovih {{objectLength}} praćenih objekata uklanja snimku, bilo koje sačuvane ugradnje, i sve povezane uloge objekata. Snimljeni materijal ovih praćenih objekata u pogledu Historija <em>NEĆE</em> biti obrisan.<br /><br />Sigurni ste da želite nastaviti?<br /><br />Zadržite tipku <em>Shift</em> da biste preskočili ovaj dijalog u budućnosti.",
"toast": {
"success": "Praćeni objekti uspješno obrisani.",
"error": "Neuspješno brisanje praćenih objekata: {{errorMessage}}"
}
},
"zoneMask": {
"filterBy": "Filtriraj po maski zone"
},
"recognizedLicensePlates": {
"title": "Prepoznate tablice",
"loadFailed": "Neuspješno učitavanje prepoznatih tablica.",
"loading": "Učitavanje prepoznatih tablica…",
"placeholder": "Unesite za pretragu tablica…",
"noLicensePlatesFound": "Nema pronađenih tablica.",
"selectPlatesFromList": "Odaberite jednu ili više tablica iz liste.",
"selectAll": "Odaberite sve",
"clearAll": "Očistite sve"
}
}
@@ -0,0 +1,8 @@
{
"iconPicker": {
"selectIcon": "Odaberite ikonu",
"search": {
"placeholder": "Pretražite ikonu…"
}
}
}
@@ -0,0 +1,10 @@
{
"button": {
"downloadVideo": {
"label": "Preuzimanje videa",
"toast": {
"success": "Vaš video stavke pregleda je započelo preuzimanje."
}
}
}
}
@@ -0,0 +1,52 @@
{
"noRecordingsFoundForThisTime": "Nisu pronađeni snimci za ovo vrijeme",
"noPreviewFound": "Nije pronađen pregled",
"noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}",
"submitFrigatePlus": {
"title": "Pošalji ovaj okvir Frigate+?",
"submit": "Pošalji",
"previewError": "Nije moguće učitati prikaz snimke. Snimka možda trenutno nije dostupna."
},
"livePlayerRequiredIOSVersion": "Za ovaj tip uživo prijenosa potreban je iOS 17.1 ili noviji.",
"streamOffline": {
"title": "Prijenos je offline",
"desc": "Nisu primljeni okviri na {{cameraName}} <code>detect</code> prijenos, provjerite zapise o greškama"
},
"cameraDisabled": "Kamera je onemogućena",
"stats": {
"streamType": {
"title": "Tip prijenosa:",
"short": "Tip"
},
"bandwidth": {
"title": "Širina pojasa:",
"short": "Širina pojasa"
},
"latency": {
"title": "Kasnjenje:",
"value": "{{seconds}} sekundi",
"short": {
"title": "Kasnjenje",
"value": "{{seconds}} sek"
}
},
"totalFrames": "Ukupno okvira:",
"droppedFrames": {
"title": "Izgubljeni okviri:",
"short": {
"title": "Izgubljeni",
"value": "{{droppedFrames}} okvira"
}
},
"decodedFrames": "Dekodirani okviri:",
"droppedFrameRate": "Stopa izgubljenih okvira:"
},
"toast": {
"success": {
"submittedFrigatePlus": "Uspješno je poslano okvir Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Neuspješno slanje okvira Frigate+"
}
}
}
+949
View File
@@ -0,0 +1,949 @@
{
"label": "KameraKonfig",
"zones": {
"label": "Zone",
"description": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.",
"friendly_name": {
"label": "Ime zone",
"description": "Korisničko ime za zonu, prikazano u UI Frigate. Ako nije postavljeno, koristi se oblikovana verzija imena zone."
},
"enabled": {
"label": "Omogućeno",
"description": "Omogući ili onemogući ovu zonu. Onemogućene zone zanemaruju se tijekom izvršavanja."
},
"enabled_in_config": {
"label": "Zapamti originalno stanje zone."
},
"filters": {
"label": "Filtri zone",
"description": "Filtri za primjenu na objekte unutar ove zone. Koriste se za smanjenje lažnih pozitiva ili ograničavanje kojih objekata se smatraju prisutnim u zoni.",
"min_area": {
"label": "Minimalna površina objekta",
"description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"max_area": {
"label": "Maksimalna površina objekta",
"description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"min_ratio": {
"label": "Minimalni omjer visine/širine",
"description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
},
"max_ratio": {
"label": "Maksimalni omjer visine/širine",
"description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
},
"threshold": {
"label": "Prag pouzdanosti",
"description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
},
"min_score": {
"label": "Minimalna pouzdanost",
"description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
},
"mask": {
"label": "Maska filtriranja",
"description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"coordinates": {
"label": "Koordinate",
"description": "Koordinate poligona koje definiraju područje zone. Može biti niz razdvojen zarezom ili lista nizova koordinata. Koordinate trebaju biti relativne (0-1) ili apsolutne (stariji format)."
},
"distances": {
"label": "Stvarne udaljenosti",
"description": "Nepovlačni stvarne udaljenosti za svaku stranu kvadrilateralne zone, koristi se za izračun brzine ili udaljenosti. Moraju imati tačno 4 vrijednosti ako su postavljene."
},
"inertia": {
"label": "Okviri inertnosti",
"description": "Broj uzastopnih okvira u kojima mora biti detektovan objekt u zoni da bi bio smatravan prisutnim. Pomaže u filtriranju privremenih detekcija."
},
"loitering_time": {
"label": "Sekunde loiteranja",
"description": "Broj sekundi koje objekt mora ostati u zoni da bi bio smatravan loiteranjem. Postaviti na 0 za onemogućavanje detekcije loiteranja."
},
"speed_threshold": {
"label": "Minimalna brzina",
"description": "Minimalna brzina (u stvarnim jedinicama ako su udaljenosti postavljene) potrebna da bi objekt bio smatravan prisutnim u zoni. Koristi se za zone koje se aktiviraju na osnovu brzine."
},
"objects": {
"label": "Objekti koji izazivaju",
"description": "Lista tipova objekata (iz labelmapa) koji mogu izazvati ovu zonu. Može biti niz ili lista nizova. Ako je prazna, svi objekti se uzimaju u obzir."
}
},
"name": {
"label": "Ime kamere",
"description": "Ime kamere je obavezno"
},
"friendly_name": {
"label": "Prijateljsko ime",
"description": "Prijateljsko ime kamere korišteno u korisničkom sučelju Frigate"
},
"enabled": {
"label": "Omogućeno",
"description": "Omogućeno"
},
"audio": {
"label": "Audio događaji",
"description": "Postavke za detekciju događaja temeljene na audio.",
"enabled": {
"label": "Omogući detekciju zvuka",
"description": "Omogući ili onemogući detekciju događaja temeljenu na audio za ovu kameru."
},
"max_not_heard": {
"label": "Vrijeme trajanja do kraja",
"description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi."
},
"min_volume": {
"label": "Minimalna zapremina",
"description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)."
},
"listen": {
"label": "Tipovi slušanja",
"description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)."
},
"filters": {
"label": "Audio filteri",
"description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva."
},
"enabled_in_config": {
"label": "Originalno stanje zvuka",
"description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci."
},
"num_threads": {
"label": "Dretve detekcije",
"description": "Broj dretvi za korištenje za obradu detekcije zvuka."
}
},
"audio_transcription": {
"label": "Transkripcija zvuka",
"description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.",
"enabled": {
"label": "Omogući transkripciju",
"description": "Omogući ili onemogući transkripciju audio događaja pokrenutu ručno."
},
"enabled_in_config": {
"label": "Originalni stanje transkripcije"
},
"live_enabled": {
"label": "Uživo transkripcija",
"description": "Omogući streaming uživo transkripcije za audio dok se prima."
}
},
"birdseye": {
"label": "Birdseye",
"description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.",
"enabled": {
"label": "Omogući Birdseye",
"description": "Omogući ili onemogući funkciju prikaza Birdseye."
},
"mode": {
"label": "Način praćenja",
"description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'."
},
"order": {
"label": "Pozicija",
"description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye."
}
},
"detect": {
"label": "Detekcija objekata",
"description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.",
"enabled": {
"label": "Omogući detekciju objekata",
"description": "Omogući ili onemogući detekciju objekata za ovu kameru."
},
"height": {
"label": "Visina detekcije",
"description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
},
"width": {
"label": "Širina detekcije",
"description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
},
"fps": {
"label": "Detekcija FPS",
"description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)."
},
"min_initialized": {
"label": "Minimalni broj okvira inicijalizacije",
"description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2."
},
"max_disappeared": {
"label": "Maksimalni broj okvira koji su nestali",
"description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim."
},
"stationary": {
"label": "Konfiguracija stacionarnih objekata",
"description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.",
"interval": {
"label": "Stacionarni interval",
"description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt."
},
"threshold": {
"label": "Stacionarni prag",
"description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni."
},
"max_frames": {
"label": "Maksimalni snimci",
"description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.",
"default": {
"label": "Zadani maksimalni snimci",
"description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka."
},
"objects": {
"label": "Maksimalni snimci po objektu",
"description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata."
}
},
"classifier": {
"label": "Omogući vizualni klasifikator",
"description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu."
}
},
"annotation_offset": {
"label": "Pomak oznake",
"description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan."
}
},
"face_recognition": {
"label": "Prepoznavanje lica",
"description": "Postavke za detekciju i prepoznavanje lica za ovu kameru.",
"enabled": {
"label": "Omogući prepoznavanje lica",
"description": "Omogući ili onemogući prepoznavanje lica."
},
"min_area": {
"label": "Minimalna površina lica",
"description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja."
}
},
"ffmpeg": {
"label": "FFmpeg",
"description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.",
"path": {
"label": "Putanja do FFmpeg binarne datoteke",
"description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")."
},
"global_args": {
"label": "Globalni argumenti FFmpeg-a",
"description": "Globalni argumenti prebačeni na procese FFmpeg."
},
"hwaccel_args": {
"label": "Argumenti za ubrzanje hardvera",
"description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača."
},
"input_args": {
"label": "Unos argumenata",
"description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg."
},
"output_args": {
"label": "Izlazni argumenti",
"description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.",
"detect": {
"label": "Izlazni argumenti za detekciju",
"description": "Zadani izlazni argumenti za snimke uloga detekcije."
},
"record": {
"label": "Izlazni argumenti za snimanje",
"description": "Zadani izlazni argumenti za snimke uloga snimanja."
}
},
"retry_interval": {
"label": "Vrijeme ponovnog pokušaja FFmpeg-a",
"description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10."
},
"apple_compatibility": {
"label": "Kompatibilnost s Apple-om",
"description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265."
},
"gpu": {
"label": "Indeks GPU-a",
"description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan."
},
"inputs": {
"label": "Ulazni podaci kamere",
"description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.",
"path": {
"label": "Putanja ulaza",
"description": "URL ili putanja ulaznog toka kamere."
},
"roles": {
"label": "Uloge ulaza",
"description": "Uloge za ovaj ulazni tok."
},
"global_args": {
"label": "Globalni argumenti FFmpeg-a",
"description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok."
},
"hwaccel_args": {
"label": "Argumenti za ubrzanje hardvera",
"description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream."
},
"input_args": {
"label": "Unos argumenata",
"description": "Argumeti unosa specifični za ovaj stream."
}
}
},
"live": {
"label": "Uživo prikaz",
"description": "Postavke korištenje Web UI za kontrolu izbora živog streama, rezolucije i kvalitete.",
"streams": {
"label": "Imena živih streamova",
"description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz."
},
"height": {
"label": "Visina uživo",
"description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama."
},
"quality": {
"label": "Kvalitet uživo",
"description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)."
}
},
"lpr": {
"label": "Prepoznavanje tablice vozila",
"description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.",
"enabled": {
"label": "Omogući LPR",
"description": "Omogući ili onemogući LPR na ovoj kameri."
},
"expire_time": {
"label": "Sekunde isteka",
"description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)."
},
"min_area": {
"label": "Minimalna površina tablice",
"description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja."
},
"enhancement": {
"label": "Nivo poboljšanja",
"description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom."
}
},
"motion": {
"label": "Detekcija pokreta",
"description": "Zadane postavke detekcije pokreta za ovu kameru.",
"enabled": {
"label": "Omogući detekciju pokreta",
"description": "Omogući ili onemogući detekciju pokreta za ovu kameru."
},
"threshold": {
"label": "Prag pokreta",
"description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)."
},
"lightning_threshold": {
"label": "Prag munje",
"description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja."
},
"skip_motion_threshold": {
"label": "Preskoči prag pokreta",
"description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije."
},
"improve_contrast": {
"label": "Poboljšaj kontrast",
"description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije."
},
"contour_area": {
"label": "Površina kontura",
"description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta."
},
"delta_alpha": {
"label": "Delta alfa",
"description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta."
},
"frame_alpha": {
"label": "Alfa okvira",
"description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta."
},
"frame_height": {
"label": "Visina okvira",
"description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta."
},
"mask": {
"label": "Koordinate maska",
"description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja."
},
"mqtt_off_delay": {
"label": "MQTT zakasnjenje isključivanja",
"description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje."
},
"enabled_in_config": {
"label": "Originalno stanje pokreta",
"description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"objects": {
"label": "Objekti",
"description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.",
"track": {
"label": "Objekti za praćenje",
"description": "Popis oznaka objekata za praćenje za ovu kameru."
},
"filters": {
"label": "Filtar objekata",
"description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).",
"min_area": {
"label": "Minimalna površina objekta",
"description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"max_area": {
"label": "Maksimalna površina objekta",
"description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
},
"min_ratio": {
"label": "Minimalni omjer visine/širine",
"description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
},
"max_ratio": {
"label": "Maksimalni omjer visine/širine",
"description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
},
"threshold": {
"label": "Prag pouzdanosti",
"description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
},
"min_score": {
"label": "Minimalna pouzdanost",
"description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
},
"mask": {
"label": "Maska filtriranja",
"description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
},
"raw_mask": {
"label": "Ručna maska"
}
},
"mask": {
"label": "Maska objekta",
"description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima."
},
"raw_mask": {
"label": "Ručna maska"
},
"genai": {
"label": "Konfiguracija GenAI objekta",
"description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.",
"enabled": {
"label": "Omogući GenAI",
"description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama."
},
"use_snapshot": {
"label": "Koristi snimke",
"description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI."
},
"prompt": {
"label": "Naslovni prompt",
"description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI."
},
"object_prompts": {
"label": "Prompti za objekte",
"description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake."
},
"objects": {
"label": "GenAI objekti",
"description": "Popis oznaka objekata koje se po defaultu šalju GenAI."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI."
},
"debug_save_thumbnails": {
"label": "Sačuvajte miniaturne slike",
"description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled."
},
"send_triggers": {
"label": "GenAI izazivači",
"description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).",
"tracked_object_end": {
"label": "Pošalji na kraju",
"description": "Pošalji zahtjev GenAI kada praćeni objekt završi."
},
"after_significant_updates": {
"label": "Raniji GenAI izazivač",
"description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt."
}
},
"enabled_in_config": {
"label": "Originalno stanje GenAI",
"description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji."
}
}
},
"record": {
"label": "Snimanje",
"description": "Postavke snimanja i zadržavanja za ovu kameru.",
"enabled": {
"label": "Omogući snimanje",
"description": "Omogući ili onemogući snimanje za ovu kameru."
},
"expire_interval": {
"label": "Interval čišćenja snimanja",
"description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka."
},
"continuous": {
"label": "Neprekidna retencija",
"description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Dana za čuvanje snimaka."
}
},
"motion": {
"label": "Retencija pokreta",
"description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Dana za čuvanje snimaka."
}
},
"detections": {
"label": "Retencija detekcije",
"description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.",
"pre_capture": {
"label": "Sekundi pre snimanja",
"description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
},
"post_capture": {
"label": "Sekunde nakon snimanja",
"description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
},
"retain": {
"label": "Zadržavanje događaja",
"description": "Postavke zadržavanja za snimke događaja detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
}
}
},
"alerts": {
"label": "Retencija upozorenja",
"description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.",
"pre_capture": {
"label": "Sekundi pre snimanja",
"description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
},
"post_capture": {
"label": "Sekunde nakon snimanja",
"description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
},
"retain": {
"label": "Zadržavanje događaja",
"description": "Postavke zadržavanja za snimke događaja detekcije.",
"days": {
"label": "Dane zadržavanja",
"description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
}
}
},
"export": {
"label": "Konfiguracija izvoza",
"description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.",
"hwaccel_args": {
"label": "Argumeti ubrzavanja dretve za izvoz",
"description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa."
},
"max_concurrent": {
"label": "Maksimalan broj istovremenih izvoza",
"description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno."
}
},
"preview": {
"label": "Konfiguracija pregleda",
"description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.",
"quality": {
"label": "Kvaliteta pregleda",
"description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)."
}
},
"enabled_in_config": {
"label": "Originalno stanje snimanja",
"description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji."
}
},
"review": {
"label": "Pregled",
"description": "Postavke koje kontrolišu upozorenja, detekcije i sažetke pregleda GenAI korišteni od strane UI i skladišta za ovu kameru.",
"alerts": {
"label": "Konfiguracija upozorenja",
"description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.",
"enabled": {
"label": "Omogući upozorenja",
"description": "Omogući ili onemogući generisanje upozorenja za ovu kameru."
},
"labels": {
"label": "Oznake upozorenja",
"description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu."
},
"enabled_in_config": {
"label": "Originalno stanje upozorenja",
"description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji."
},
"cutoff_time": {
"label": "Vrijeme prekida upozorenja",
"description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje."
}
},
"detections": {
"label": "Konfiguracija detekcija",
"description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.",
"enabled": {
"label": "Omogući detekcije",
"description": "Omogući ili onemogući događaje detekcije za ovu kameru."
},
"labels": {
"label": "Oznake detekcije",
"description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu."
},
"cutoff_time": {
"label": "Vrijeme prekida detekcija",
"description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija."
},
"enabled_in_config": {
"label": "Originalno stanje detekcija",
"description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji."
}
},
"genai": {
"label": "Konfiguracija GenAI",
"description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.",
"enabled": {
"label": "Omogući opise GenAI",
"description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled."
},
"alerts": {
"label": "Omogući GenAI za upozorenja",
"description": "Koristi GenAI za generiranje opisa stavki upozorenja."
},
"detections": {
"label": "Omogući GenAI za detekcije",
"description": "Koristite GenAI za generiranje opisa predmeta detekcije."
},
"image_source": {
"label": "Pregledajte izvor slike",
"description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena."
},
"additional_concerns": {
"label": "Dodatne brige",
"description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri."
},
"debug_save_thumbnails": {
"label": "Sačuvajte miniaturne slike",
"description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled."
},
"enabled_in_config": {
"label": "Originalno stanje GenAI",
"description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji."
},
"preferred_language": {
"label": "Preferirani jezik",
"description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore."
},
"activity_context_prompt": {
"label": "Prompt konteksta aktivnosti",
"description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI."
}
}
},
"semantic_search": {
"label": "Semantička pretraga",
"description": "Postavke za semantičku pretragu koja konstruira i upita uključivanje objekata kako bi pronašla slične stavke.",
"triggers": {
"label": "Pokretači",
"description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.",
"friendly_name": {
"label": "Prijateljsko ime",
"description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač."
},
"enabled": {
"label": "Omogući ovaj pokretač",
"description": "Omogući ili onemogući ovaj pokretač semantičke pretrage."
},
"type": {
"label": "Tip pokretača",
"description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)."
},
"data": {
"label": "Sadržaj pokretača",
"description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima."
},
"threshold": {
"label": "Prag aktivacije",
"description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača."
},
"actions": {
"label": "Akcije izazivača",
"description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)."
}
}
},
"snapshots": {
"label": "Snimci",
"description": "Postavke za snimke generirane preko API-ja za praćene objekte za ovu kameru.",
"enabled": {
"label": "Omogući snimke",
"description": "Omogući ili onemogući snimanje snimaka za ovu kameru."
},
"timestamp": {
"label": "Preklapanje vremenske oznake",
"description": "Preklopiti vremensku oznaku na snimke iz API-ja."
},
"bounding_box": {
"label": "Preklapanje okvira",
"description": "Crtanje okvira za praćene objekte na snimke iz API-ja."
},
"crop": {
"label": "Izrezivanje snimke",
"description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi snimka bila sačuvana."
},
"height": {
"label": "Visina snimke",
"description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu."
},
"retain": {
"label": "Zadržavanje snimki",
"description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.",
"default": {
"label": "Zadano zadržavanje",
"description": "Zadani broj dana za zadržavanje snimki."
},
"mode": {
"label": "Način zadržavanja",
"description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
},
"objects": {
"label": "Zadržavanje objekata",
"description": "Prekriženja po objektu za dane zadržavanja snimki."
}
},
"quality": {
"label": "Kvaliteta snimka",
"description": "Kvaliteta kodiranja za sačuvane snimke (0-100)."
}
},
"timestamp_style": {
"label": "Stil vremenske oznake",
"description": "Opcije stilizacije za vremenske oznake u snimcima i snimcima.",
"position": {
"label": "Pozicija vremenske oznake",
"description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)."
},
"format": {
"label": "Format vremenske oznake",
"description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)."
},
"color": {
"label": "Boja vremenske oznake",
"description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).",
"red": {
"label": "Crvena",
"description": "Crveni komponent (0-255) za boju vremenske oznake."
},
"green": {
"label": "Zelena",
"description": "Zeleni komponent (0-255) za boju vremenske oznake."
},
"blue": {
"label": "Plava",
"description": "Plavi komponent (0-255) za boju vremenske oznake."
}
},
"thickness": {
"label": "Debljina vremenske oznake",
"description": "Debljina linije teksta vremenske oznake."
},
"effect": {
"label": "Efekt vremenske oznake",
"description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)."
}
},
"best_image_timeout": {
"label": "Vrijeme čekanja za najbolju sliku",
"description": "Koliko dugo čekati na sliku s najvišim stupnjem pouzdanosti."
},
"mqtt": {
"label": "MQTT",
"description": "Postavke objave slika preko MQTT.",
"enabled": {
"label": "Pošalji sliku",
"description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru."
},
"timestamp": {
"label": "Dodaj vremensku oznaku",
"description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT."
},
"bounding_box": {
"label": "Dodaj okvir",
"description": "Crtaj okvire na slikama objavljenim preko MQTT."
},
"crop": {
"label": "Iscijepi sliku",
"description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta."
},
"height": {
"label": "Visina slike",
"description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila."
},
"quality": {
"label": "Kvaliteta JPEG",
"description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)."
}
},
"notifications": {
"label": "Obavještenja",
"description": "Postavke za omogućavanje i kontrolu obavijesti za ovu kameru.",
"enabled": {
"label": "Omogući obavijesti",
"description": "Omogući ili onemogući obavijesti za ovu kameru."
},
"email": {
"label": "E-mail za obavijesti",
"description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti."
},
"cooldown": {
"label": "Period hlađenja",
"description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima."
},
"enabled_in_config": {
"label": "Originalno stanje obavijesti",
"description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji."
}
},
"onvif": {
"label": "ONVIF",
"description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.",
"host": {
"label": "Gost ONVIF",
"description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru."
},
"port": {
"label": "Port ONVIF",
"description": "Broj porta za uslugu ONVIF."
},
"user": {
"label": "Korisničko ime za ONVIF",
"description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF."
},
"password": {
"label": "Lozinka za ONVIF",
"description": "Lozinka za autentifikaciju ONVIF."
},
"tls_insecure": {
"label": "Onemogući provjeru TLS",
"description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)."
},
"profile": {
"label": "ONVIF profil",
"description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire."
},
"autotracking": {
"label": "Autotračenje",
"description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.",
"enabled": {
"label": "Omogući automatsko praćenje",
"description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata."
},
"calibrate_on_startup": {
"label": "Kalibriraj na početku",
"description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije."
},
"zooming": {
"label": "Režim zumiranja",
"description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)."
},
"zoom_factor": {
"label": "Faktor zumiranja",
"description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75."
},
"track": {
"label": "Praćeni objekti",
"description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje."
},
"required_zones": {
"label": "Potrebne zone",
"description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje."
},
"return_preset": {
"label": "Povratak na predpostavku",
"description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja."
},
"timeout": {
"label": "Vrijeme čekanja povratka",
"description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto."
},
"movement_weights": {
"label": "Težine pokreta",
"description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno."
},
"enabled_in_config": {
"label": "Originalni stanje autotračenja",
"description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji."
}
},
"ignore_time_mismatch": {
"label": "Zanemari razliku u vremenu",
"description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF."
}
},
"type": {
"label": "Tip kamere",
"description": "Tip kamere"
},
"ui": {
"label": "Korisnički interfejs kamere",
"description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.",
"order": {
"label": "Redoslijed UI",
"description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije."
},
"dashboard": {
"label": "Prikaži u UI",
"description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI."
}
},
"webui_url": {
"label": "URL kamere",
"description": "URL za pristup kamere izravno iz stranice sustava"
},
"profiles": {
"label": "Profili",
"description": "Imenovane konfiguracijske profile s parcijalnim preklopima koji se mogu aktivirati tijekom izvršavanja."
},
"enabled_in_config": {
"label": "Originalno stanje kamere",
"description": "Pratite originalno stanje kamere."
}
}
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
{
"audio": {
"global": {
"detection": "Globalna detekcija",
"sensitivity": "Globalna osjetljivost"
},
"cameras": {
"detection": "Detekcija",
"sensitivity": "Osjetljivost"
}
},
"timestamp_style": {
"global": {
"appearance": "Globalno izgled"
},
"cameras": {
"appearance": "Izgled"
}
},
"motion": {
"global": {
"sensitivity": "Globalna osjetljivost",
"algorithm": "Globalni algoritam"
},
"cameras": {
"sensitivity": "Osjetljivost",
"algorithm": "Algoritam"
}
},
"snapshots": {
"global": {
"display": "Globalno prikazivanje"
},
"cameras": {
"display": "Prikazivanje"
}
},
"detect": {
"global": {
"resolution": "Globalna rezolucija",
"tracking": "Globalno praćenje"
},
"cameras": {
"resolution": "Rezolucija",
"tracking": "Praćenje"
}
},
"objects": {
"global": {
"tracking": "Globalno praćenje",
"filtering": "Globalno filtriranje"
},
"cameras": {
"tracking": "Praćenje",
"filtering": "Filtriranje"
}
},
"record": {
"global": {
"retention": "Globalno zadržavanje",
"events": "Globalni događaji"
},
"cameras": {
"retention": "Zadržavanje",
"events": "Događaji"
}
},
"ffmpeg": {
"cameras": {
"cameraFfmpeg": "Argumeni FFmpeg specifični za kameru"
}
}
}
@@ -0,0 +1,32 @@
{
"minimum": "Bar {{limit}}",
"maximum": "Mora biti najviše {{limit}}",
"exclusiveMinimum": "Mora biti veći od {{limit}}",
"exclusiveMaximum": "Mora biti manje od {{limit}}",
"minLength": "Bar {{limit}} znak(ovi)",
"maxLength": "Mora biti najviše {{limit}} znak(ovi)",
"minItems": "Mora imati bar {{limit}} stavke",
"maxItems": "Mora imati najviše {{limit}} stavke",
"pattern": "Neispravan format",
"required": "Ovo polje je obavezno",
"type": "Neispravan tip vrijednosti",
"enum": "Mora biti jedan od dopuštenih vrijednosti",
"const": "Vrijednost se ne podudara s očekivanom konstantom",
"uniqueItems": "Sve stavke moraju biti jedinstvene",
"format": "Neispravan format",
"additionalProperties": "Nepoznato svojstvo nije dozvoljeno",
"oneOf": "Mora se podudarati s točno jednim od dopuštenih shema",
"anyOf": "Mora se podudarati s bar jednim od dopuštenih shema",
"proxy": {
"header_map": {
"roleHeaderRequired": "Zaglavlje uloge je obavezno kada su konfigurirane mapiranja uloga."
}
},
"ffmpeg": {
"inputs": {
"rolesUnique": "Svaka uloga može biti dodijeljena samo jednom ulaznom toku.",
"detectRequired": "Bar jedan ulazni tok mora biti dodijeljen ulozi 'detektirati'.",
"hwaccelDetectOnly": "Samo ulazni tok s ulogom detektiranja može definirati argumente ubrzavanja hardvera."
}
}
}
+125
View File
@@ -0,0 +1,125 @@
{
"person": "Ljudsko bit će",
"bicycle": "Kolo",
"animal": "Životinja",
"dog": "Pas",
"bark": "Glavu",
"cat": "Mačka",
"horse": "Konj",
"goat": "Koza",
"sheep": "Ovca",
"bird": "Ptica",
"mouse": "Miš",
"keyboard": "Klaviatura",
"vehicle": "Vozilo",
"boat": "Brod",
"car": "Automobil",
"bus": "Autobus",
"motorcycle": "Motocikl",
"train": "Vlak",
"skateboard": "Skejtbord",
"door": "Vrata",
"blender": "Miksere",
"sink": "Lavabo",
"hair_dryer": "Sušilac za kosu",
"toothbrush": "Šetka za zube",
"scissors": "Škare",
"clock": "Sat",
"airplane": "Avion",
"traffic_light": "Svetofor",
"fire_hydrant": "Vatrostaničar",
"street_sign": "Ulični znak",
"stop_sign": "Znak zaustavljanja",
"parking_meter": "Parkirni metar",
"bench": "Banko",
"cow": "Korova",
"elephant": "Slon",
"bear": "Medvjed",
"zebra": "Zebra",
"giraffe": "Žirafa",
"hat": "Kaputa",
"backpack": "Torba",
"umbrella": "Kreveta",
"shoe": "Cizma",
"eye_glasses": "Očna stakla",
"handbag": "Ručna torba",
"tie": "Kremplj",
"suitcase": "Kufer",
"frisbee": "Frizbi",
"skis": "Ski",
"snowboard": "Snjegobord",
"sports_ball": "Sportska lopta",
"kite": "Let",
"baseball_bat": "Batsa za baseball",
"baseball_glove": "Rukavica za baseball",
"surfboard": "Surfbord",
"tennis_racket": "Teniski raketa",
"bottle": "Bocica",
"plate": "Ploča",
"wine_glass": "Vinsko čaša",
"cup": "Kupa",
"fork": "Škarpe",
"knife": "Nož",
"spoon": "Lajna",
"bowl": "Tanjir",
"banana": "Banana",
"apple": "Jabuka",
"sandwich": "Sandučić",
"orange": "Portakal",
"broccoli": "Brobkoli",
"carrot": "Mahunika",
"hot_dog": "Hot dog",
"pizza": "Pica",
"donut": "Krofna",
"cake": "Torta",
"chair": "Stolica",
"couch": "Divan",
"potted_plant": "Ukrasna biljka",
"bed": "Krevet",
"mirror": "Zrcalo",
"dining_table": "Stol za ručak",
"window": "Prozor",
"desk": "Radni stol",
"toilet": "Toalet",
"tv": "TV",
"laptop": "Laptop",
"remote": "Udaljeno upravljanje",
"cell_phone": "Mobilni telefon",
"microwave": "Mikrotalasna pećnica",
"oven": "Pećnica",
"toaster": "Tostera",
"refrigerator": "Hladnjak",
"book": "Knjiga",
"vase": "Vaza",
"teddy_bear": "Biberon",
"hair_brush": "Kosmetička četka",
"squirrel": "Šumski pas",
"deer": "Jelen",
"fox": "Lisica",
"rabbit": "Zajac",
"raccoon": "Rakun",
"robot_lawnmower": "Robotska kosilica",
"waste_bin": "Kanta za otpad",
"on_demand": "Na zahtjev",
"face": "Lice",
"license_plate": "Tablica",
"package": "Paket",
"bbq_grill": "Grill za BBQ",
"amazon": "Amazon",
"usps": "USPS",
"ups": "UPS",
"fedex": "FedEx",
"dhl": "DHL",
"an_post": "An Post",
"purolator": "Purolator",
"postnl": "PostNL",
"nzpost": "NZPost",
"postnord": "PostNord",
"gls": "GLS",
"dpd": "DPD",
"canada_post": "Canada Post",
"royal_mail": "Royal Mail",
"school_bus": "Školski autobus",
"skunk": "Mrdka",
"kangaroo": "Kanguru"
}
+46
View File
@@ -0,0 +1,46 @@
{
"documentTitle": "Razgovor - Frigate",
"title": "Frigate razgovor",
"subtitle": "Vaš AI asistent za upravljanje kamerama i insighti",
"placeholder": "Pitajte bilo što...",
"error": "Nešto je pošlo po zlu. Molimo pokušajte ponovo.",
"processing": "Obrađivanje...",
"toolsUsed": "Korišteno: {{tools}}",
"showTools": "Prikaži alate ({{count}})",
"hideTools": "Sakrij alate",
"call": "Poziv",
"result": "Rezultat",
"arguments": "Argumenti:",
"response": "Odgovor:",
"attachment_chip_label": "{{label}} na {{camera}}",
"attachment_chip_remove": "Ukloni privitak",
"open_in_explore": "Otvori u Explore",
"attach_event_aria": "Prikači događaj {{eventId}}",
"attachment_picker_paste_label": "Ili zalijepite ID događaja",
"attachment_picker_attach": "Prikači",
"attachment_picker_placeholder": "Prikači događaj",
"quick_reply_find_similar": "Pronađi slične susreti",
"quick_reply_tell_me_more": "Recite mi više o ovome",
"quick_reply_when_else": "Kada je još puta vidjeno?",
"quick_reply_find_similar_text": "Pronađi slične susreti za ovaj.",
"quick_reply_tell_me_more_text": "Recite mi više o ovom.",
"quick_reply_when_else_text": "Kada je to još puta vidjeno?",
"anchor": "Referenca",
"similarity_score": "Sličnost",
"no_similar_objects_found": "Nisu pronađeni slični objekti.",
"semantic_search_required": "Semantička pretraga mora biti omogućena da bi se pronašli slični objekti.",
"send": "Pošalji",
"suggested_requests": "Pokušaj pitati:",
"starting_requests": {
"show_recent_events": "Prikaži nedavne događaje",
"show_camera_status": "Prikaži status kamere",
"recap": "Što se desilo dok sam bio odsutan?",
"watch_camera": "Pratite kameru za aktivnost"
},
"starting_requests_prompts": {
"show_recent_events": "Prikaži mi nedavne događaje iz posljednjeg sata",
"show_camera_status": "Koji je trenutni status mojih kamera?",
"recap": "Što se desilo dok sam bio odsutan?",
"watch_camera": "Pratite ulazna vrata i obavijestite me ako netko dođe"
}
}
@@ -0,0 +1,205 @@
{
"documentTitle": "Modeli klasifikacije - Frigate",
"details": {
"scoreInfo": "Ocjena predstavlja prosjek pouzdanosti klasifikacije kroz sve detekcije ovog objekta.",
"none": "Nijedan",
"unknown": "Nepoznato"
},
"button": {
"deleteClassificationAttempts": "Obriši slike klasifikacije",
"renameCategory": "Preimenuj klasu",
"deleteCategory": "Obriši klasu",
"deleteImages": "Obriši slike",
"trainModel": "Obuci model",
"addClassification": "Dodaj klasifikaciju",
"deleteModels": "Obriši modele",
"editModel": "Uredi model"
},
"tooltip": {
"trainingInProgress": "Model trenutno obučava",
"noNewImages": "Nema novih slika za obuku. Prvo klasificirajte više slika u skupu podataka.",
"noChanges": "Nema promjena u skupu podataka od posljednje obuke.",
"modelNotReady": "Model nije spreman za obuku"
},
"toast": {
"success": {
"deletedModel_one": "Uspješno obrisan {{count}} model",
"deletedModel_few": "Uspješno obrisani {{count}} modeli",
"deletedModel_other": "Uspješno obrisani {{count}} modeli",
"categorizedImage": "Uspješno klasificirana slika",
"reclassifiedImage": "Uspješno ponovno klasificirana slika",
"trainedModel": "Uspješno obučen model.",
"trainingModel": "Uspješno pokrenuta obuka modela.",
"updatedModel": "Uspješno ažurirana konfiguracija modela",
"renamedCategory": "Uspješno preimenovana klasa u {{name}}",
"deletedCategory_one": "Obrisana {{count}} klasa",
"deletedCategory_few": "Obrisane {{count}} klase",
"deletedCategory_other": "Obrisane {{count}} klase",
"deletedImage_one": "Izbrisana {{count}} slika",
"deletedImage_few": "Izbrisane {{count}} slike",
"deletedImage_other": "Izbrisane {{count}} slike"
},
"error": {
"deleteImageFailed": "Neuspješno brisanje: {{errorMessage}}",
"deleteCategoryFailed": "Neuspješno brisanje klase: {{errorMessage}}",
"deleteModelFailed": "Neuspješno brisanje modela: {{errorMessage}}",
"categorizeFailed": "Neuspješno kategoriziranje slike: {{errorMessage}}",
"trainingFailed": "Obuka modela nije uspješna. Provjerite zapise Frigate za detalje.",
"trainingFailedToStart": "Neuspješno pokretanje obuke modela: {{errorMessage}}",
"updateModelFailed": "Neuspješno ažuriranje modela: {{errorMessage}}",
"renameCategoryFailed": "Neuspješno preimenovanje klase: {{errorMessage}}",
"reclassifyFailed": "Neuspješno ponovno klasifikovanje slike: {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Izbriši klasu",
"desc": "Sigurni li ste da želite izbrisati klasu {{name}}? Ovo će trajno izbrisati sve povezane slike i zahtijevati ponovnu obuku modela.",
"minClassesTitle": "Nemoguće izbrisati klasu",
"minClassesDesc": "Model klasifikacije mora imati najmanje 2 klase. Dodajte još jednu klasu prije brisanja ove."
},
"deleteModel": {
"title": "Izbriši model klasifikacije",
"single": "Sigurni li ste da želite izbrisati {{name}}? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_one": "Sigurni li ste da želite izbrisati {{count}} model? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_few": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
"desc_other": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena."
},
"edit": {
"title": "Uredi model klasifikacije",
"descriptionState": "Uredi klase za ovaj model klasifikacije stanja. Promjene će zahtijevati ponovnu obuku modela.",
"descriptionObject": "Uredi vrstu objekta i vrstu klasifikacije za ovaj model klasifikacije objekta.",
"stateClassesInfo": "Napomena: Promjene klasa stanja zahtijevaju ponovnu obuku modela sa ažuriranim klasama."
},
"deleteDatasetImages": {
"title": "Izbriši slike skupa podataka",
"desc_one": "Sigurni li ste da želite izbrisati {{count}} sliku iz {{dataset}}? Ova akcija ne može biti poništena i zahtijevati će ponovnu obuku modela.",
"desc_few": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela.",
"desc_other": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela."
},
"deleteTrainImages": {
"title": "Obriši slike za treniranje",
"desc_one": "Sigurni li ste da želite obrisati {{count}} sliku? Ova akcija ne može se poništiti.",
"desc_few": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti.",
"desc_other": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti."
},
"renameCategory": {
"title": "Preimenuj klasu",
"desc": "Unesite novo ime za {{name}}. Za promjenu imena će vam se zahtijevati ponovno treniranje modela."
},
"description": {
"invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte."
},
"train": {
"title": "Nedavne klasifikacije",
"titleShort": "Nedavno",
"aria": "Odaberite nedavne klasifikacije"
},
"categories": "Klase",
"createCategory": {
"new": "Stvori novu klasu"
},
"categorizeImageAs": "Klasificiraj sliku kao:",
"categorizeImage": "Klasificiraj sliku",
"reclassifyImageAs": "Ponovno klasificiraj sliku kao:",
"reclassifyImage": "Ponovno klasificiraj sliku",
"menu": {
"objects": "Objekti",
"states": "Stanja"
},
"noModels": {
"object": {
"title": "Nema modela za klasifikaciju objekata",
"description": "Stvorite prilagođeni model za klasifikaciju detektiranih objekata.",
"buttonText": "Stvori model objekta"
},
"state": {
"title": "Nema modela za klasifikaciju stanja",
"description": "Stvorite prilagođeni model za praćenje i klasifikaciju promjena stanja u određenim područjima kamere.",
"buttonText": "Stvori model stanja"
}
},
"wizard": {
"title": "Stvori novu klasifikaciju",
"steps": {
"nameAndDefine": "Ime i definicija",
"stateArea": "Područje stanja",
"chooseExamples": "Odaberite primjere"
},
"step1": {
"description": "Modeli stanja nadziraju fiksne područja kamere za promjene (npr. vrata otvorena/zatvorena). Modeli objekata dodaju klasifikacije detektiranim objektima (npr. poznati životinje, dostavljači, itd.).",
"name": "Ime",
"namePlaceholder": "Unesite ime modela...",
"type": "Tip",
"typeState": "Stanje",
"typeObject": "Objekt",
"objectLabel": "Oznaka objekta",
"objectLabelPlaceholder": "Odaberite vrstu objekta...",
"classificationType": "Vrsta klasifikacije",
"classificationTypeTip": "Učite više o vrstama klasifikacije",
"classificationTypeDesc": "Podoznake dodaju dodatni tekst oznaci objekta (npr. 'Ljudsko bit će: UPS'). Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima objekta.",
"classificationSubLabel": "Podoznaka",
"classificationAttribute": "Atribut",
"classes": "Klase",
"states": "Stanja",
"classesTip": "Učite više o klasama",
"classesStateDesc": "Definirajte različita stanja koja može imati područje kamere. Na primjer: 'otvoreno' i 'zatvoreno' za vrata garaže.",
"classesObjectDesc": "Definirajte različite kategorije u koje ćete klasificirati detektirane objekte. Na primjer: 'dostavljac', 'stanovnik', 'stranac' za klasifikaciju ljudi.",
"classPlaceholder": "Unesite ime klase...",
"errors": {
"nameRequired": "Ime modela je obavezno",
"nameLength": "Ime modela mora imati 64 znaka ili manje",
"nameOnlyNumbers": "Ime modela ne može sadržavati samo brojeve",
"classRequired": "Potrebna je bar jedna klasa",
"classesUnique": "Imena klasa moraju biti jedinstvena",
"noneNotAllowed": "Klasa 'none' nije dozvoljena",
"stateRequiresTwoClasses": "Modeli stanja zahtijevaju bar dvije klase",
"objectLabelRequired": "Molimo odaberite oznaku objekta",
"objectTypeRequired": "Molimo odaberite vrstu klasifikacije"
}
},
"step2": {
"description": "Odaberite kamere i definirajte područje koje ćete nadzirati za svaku kameru. Model će klasificirati stanje ovih područja.",
"cameras": "Kamere",
"selectCamera": "Odaberite Kameru",
"noCameras": "Kliknite + za dodavanje kamera",
"selectCameraPrompt": "Odaberite kameru iz popisa da biste definirali njezino područje nadzora"
},
"step3": {
"selectImagesPrompt": "Odaberite sve slike s: {{className}}",
"selectImagesDescription": "Kliknite na slike da biste ih odabrali. Kliknite Nadalje kada završite s ovom klasifikacijom.",
"allImagesRequired_one": "Molimo klasificirajte sve slike. Preostala je {{count}} slika.",
"allImagesRequired_few": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
"allImagesRequired_other": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
"generating": {
"title": "Generisanje uzoraka slika",
"description": "Frigate učitava reprezentativne slike iz vaših snimaka. Ovo može trajati trenutak..."
},
"training": {
"title": "Obučavanje modela",
"description": "Vaš model se trenutno obučava u pozadini. Zatvorite ovaj dijalog, a vaš model će započeti raditi odmah kada se obuka završi."
},
"retryGenerate": "Ponovno generisanje",
"noImages": "Nema generisanih uzoraka slika",
"classifying": "Klasifikacija i obuka...",
"trainingStarted": "Obuka je uspješno pokrenuta",
"modelCreated": "Model je uspješno stvoren. Koristite pogled Najnovije klasifikacije da dodate slike za nedostajuće stanja, a zatim obučite model.",
"errors": {
"noCameras": "Nema konfigurisanih kamera",
"noObjectLabel": "Nije odabrana oznaka objekta",
"generateFailed": "Neuspješno generisanje primera: {{error}}",
"generationFailed": "Generisanje nije uspješno. Molimo pokušajte ponovo.",
"classifyFailed": "Neuspješna klasifikacija slika: {{error}}"
},
"generateSuccess": "Uspješno generisane uzorak slike",
"refreshExamples": "Generiši nove primjere",
"refreshConfirm": {
"title": "Generiši nove primjere?",
"description": "Ovo će generisati novi skup slika i obrisati sve odabire, uključujući prethodne klase. Trebat će vam ponovno odabrati primjere za sve klase."
},
"missingStatesWarning": {
"title": "Primjeri nedostajućih klasa",
"description": "Nisu sve klase imale primjere. Pokušajte generisanje novih primjera da biste pronašli nedostajuću klasu, ili nastavite i koristite pogled Najnovije klasifikacije da biste kasnije dodali slike."
}
}
}
}
@@ -0,0 +1,18 @@
{
"documentTitle": "Uređivač konfiguracije - Frigate",
"configEditor": "Uređivač konfiguracije",
"safeConfigEditor": "Uređivač konfiguracije (Sigurnosni režim)",
"safeModeDescription": "Frigate je u sigurnosnom režimu zbog greške u validaciji konfiguracije.",
"copyConfig": "Kopiraj konfiguraciju",
"saveAndRestart": "Sačuvaj i ponovo pokreni",
"saveOnly": "Sačuvaj samo",
"confirm": "Napusti bez čuvanja?",
"toast": {
"success": {
"copyToClipboard": "Konfiguracija kopirana u međuspremnik."
},
"error": {
"savingError": "Greška prilikom čuvanja konfiguracije"
}
}
}
+92
View File
@@ -0,0 +1,92 @@
{
"alerts": "Upozorenja",
"detections": "Detekcije",
"camera": "Kamera",
"motion": {
"label": "Kretanje",
"only": "Samo pokret"
},
"allCameras": "Sve Kamere",
"empty": {
"alert": "Nema upozorenja za pregled",
"detection": "Nema detekcija za pregled",
"motion": "Nema podataka o pokretu",
"recordingsDisabled": {
"title": "Snimci moraju biti omogućeni",
"description": "Pregledni stavci mogu se stvarati samo za kameru kada su snimci omogućeni za tu kameru."
}
},
"timeline": {
"label": "vremenska linija",
"aria": "Odaberite vremensku liniju"
},
"zoomIn": "Uvećajte",
"zoomOut": "Umanjite",
"events": {
"label": "Događaji",
"aria": "Odaberite događaje",
"noFoundForTimePeriod": "Nema događaja za ovaj vremenski period."
},
"detail": {
"label": "Detalj",
"noDataFound": "Nema detaljnih podataka za pregled",
"aria": "Prekidač pregleda detalja",
"trackedObject_one": "{{count}} objekt",
"trackedObject_other": "{{count}} objekta",
"noObjectDetailData": "Nema dostupnih detaljnih podataka o objektu.",
"settings": "Postavke pregleda detalja",
"alwaysExpandActive": {
"title": "Uvijek proširujte aktivno",
"desc": "Uvijek proširite detalje objekta aktivnog stavka pregleda kada su dostupni."
}
},
"objectTrack": {
"trackedPoint": "Praćeni točka",
"clickToSeek": "Kliknite da biste prešli na ovo vrijeme"
},
"documentTitle": "Pregled - Frigate",
"recordings": {
"documentTitle": "Snimci - Frigate",
"invalidSharedLink": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog greške u parsiranju.",
"invalidSharedCamera": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog nepoznate ili neovlašćene kamere."
},
"calendarFilter": {
"last24Hours": "Poslednje 24 sata"
},
"markAsReviewed": "Označi kao pregledano",
"markTheseItemsAsReviewed": "Označi ove stavke kao pregledane",
"newReviewItems": {
"label": "Pregledaj nove stavke za pregled",
"button": "Nove stavke za pregled"
},
"selected_one": "{{count}} odabrano",
"selected_other": "{{count}} odabrano",
"select_all": "Sve",
"detected": "detektovano",
"normalActivity": "Normal",
"needsReview": "Treba pregledati",
"securityConcern": "Sigurnosna zabrinutost",
"motionSearch": {
"menuItem": "Pretraga kretanja",
"openMenu": "Opcije kamere"
},
"motionPreviews": {
"menuItem": "Pregledaj preglednike kretanja",
"title": "Preglednici kretanja: {{camera}}",
"mobileSettingsTitle": "Postavke preglednika kretanja",
"mobileSettingsDesc": "Prilagodite brzinu reprodukcije i osvetljavanje, i odaberite datum za pregled snimaka samo sa kretanjem.",
"dim": "Osvetljavanje",
"dimAria": "Prilagodite intenzitet osvetljavanja",
"dimDesc": "Povećajte osvetljavanje da biste povećali vidljivost područja kretanja.",
"speed": "Brzina",
"speedAria": "Odaberite brzinu reprodukcije preglednika",
"speedDesc": "Odaberite koliko brzo će se pregledni snimci reproducirati.",
"back": "Nazad",
"empty": "Nema pregleda dostupnih",
"noPreview": "Pregled nije dostupan",
"seekAria": "Pretражuj {{camera}} igraču do {{time}}",
"filter": "Filtar",
"filterDesc": "Odaberite područja da biste prikazali samo klipove sa kretanjem u tim područjima.",
"filterClear": "Očisti"
}
}
+267
View File
@@ -0,0 +1,267 @@
{
"documentTitle": "Istraživanje - Frigate",
"generativeAI": "Generativna AI",
"exploreMore": "Istražite više {{label}} objekata",
"exploreIsUnavailable": {
"title": "Istraživanje nije dostupno",
"embeddingsReindexing": {
"context": "Istraživanje može se koristiti nakon što se reindeksiranje ugrađenih objekata završi.",
"startingUp": "Pokretanje…",
"estimatedTime": "Procijenjeno preostalo vrijeme:",
"finishingShortly": "Završetak uskoro",
"step": {
"thumbnailsEmbedded": "Ugrađene miniaturne slike: ",
"descriptionsEmbedded": "Ugrađene opise: ",
"trackedObjectsProcessed": "Obrađeni praćeni objekti: "
}
},
"downloadingModels": {
"context": "Frigate preuzima potrebne modele ugrađenih objekata kako bi podržao funkciju Semantičke pretrage. Ovo može trajati nekoliko minuta ovisno o brzini vaše mreže.",
"setup": {
"visionModel": "Model vida",
"visionModelFeatureExtractor": "Izvođač značajki modela vida",
"textModel": "Model teksta",
"textTokenizer": "Tokenizator teksta"
},
"tips": {
"context": "Moguće je da želite ponovno indeksirati ugrađene objekte koji se prate nakon što se modele preuzmu."
},
"error": "Dogodila se greška. Provjerite zapise Frigate."
}
},
"trackedObjectDetails": "Detalji praćenih objekata",
"type": {
"details": "Detalji",
"snapshot": "Snimak",
"thumbnail": "miniaturna slika",
"video": "Video",
"tracking_details": "detalji praćenja"
},
"trackingDetails": {
"title": "Detalji praćenja",
"noImageFound": "Nije pronađena slika za ovaj vremenski moment.",
"createObjectMask": "Kreirajte masku objekta",
"adjustAnnotationSettings": "Prilagodite postavke oznaka",
"scrollViewTips": "Kliknite da biste vidjeli važne trenutke životnog ciklusa ovog objekta.",
"autoTrackingTips": "Pozicije okvirnih kutija neće biti tačne za autotracking kamere.",
"count": "{{first}} od {{second}}",
"trackedPoint": "Praćena tačka",
"lifecycleItemDesc": {
"visible": "{{label}} detektovan",
"entered_zone": "{{label}} ušao u {{zones}}",
"active": "{{label}} postao aktivno",
"stationary": "{{label}} postao stacionarno",
"attribute": {
"faceOrLicense_plate": "{{attribute}} detektovan za {{label}}",
"other": "{{label}} prepoznat kao {{attribute}}"
},
"gone": "{{label}} otišao",
"heard": "{{label}} čujeo",
"external": "{{label}} detektovan",
"header": {
"zones": "Zone",
"ratio": "Omjer",
"area": "Površina",
"score": "Rezultat",
"computedScore": "Izračunata ocjena",
"topScore": "Najbolja ocjena",
"toggleAdvancedScores": "Prekidač naprednih ocjena"
}
},
"annotationSettings": {
"title": "Postavke oznaka",
"showAllZones": {
"title": "Prikaži sve zone",
"desc": "Uvijek prikazujte zone na okvirima gdje su objekti ušli u zonu."
},
"offset": {
"label": "Pomak oznaka",
"desc": "Ova podatka dolaze iz vaše kamere detektovane snimke, ali se preklapaju na slikama iz snimke snimke. Vjerojatno nije moguće da su dva toka savršeno sinhronizirana. Kao rezultat, okvirni kutiji i snimke neće se savršeno poklopiti. Možete koristiti ovu postavku da pomaknete oznake unaprijed ili unazad u vremenu da bi ih bolje uskladili s snimljenim snimkom.",
"millisecondsToOffset": "Milisekunde za pomak detektovanih oznaka. <em>Podrazumevano: 0</em>",
"tips": "Smanjite vrijednost ako je reprodukcija videa ispred kutija i tačaka putanje, a povećajte vrijednost ako je reprodukcija videa iza njih. Ova vrijednost može biti negativna.",
"toast": {
"success": "Pomak anotacije za {{camera}} je sačuvan u konfiguracionu datoteku."
}
}
},
"carousel": {
"previous": "Prethodni slajd",
"next": "Sljedeći slajd"
}
},
"details": {
"item": {
"title": "Pregled detalja stavke",
"desc": "Detalji stavke za pregled",
"button": {
"share": "Dijelite ovu stavku za pregled",
"viewInExplore": "Pregledajte u Explore"
},
"tips": {
"mismatch_one": "{{count}} nedostupan objekat je detektovan i uključen u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekcija, ili su već očišćeni/obrisani.",
"mismatch_few": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
"mismatch_other": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
"hasMissingObjects": "Prilagodite svoju konfiguraciju ako želite da Frigate sačuva pratiti objekte za sljedeće oznake: <em>{{objects}}</em>"
},
"toast": {
"success": {
"regenerate": "Zahtjev za novi opis je poslat {{provider}}. Ovisno o brzini vašeg provajdera, novi opis može potrajati neko vrijeme da se ponovno generira.",
"updatedSublabel": "Uspješno ažurirana podjezika.",
"updatedLPR": "Uspješno ažurirana tablica.",
"updatedAttributes": "Uspješno ažurirana atribute.",
"audioTranscription": "Uspješno zahtjev za audio transkripciju. Ovisno o brzini vašeg Frigate servera, transkripcija može potrajati neko vrijeme da se završi."
},
"error": {
"regenerate": "Neuspješno poziv {{provider}} za novi opis: {{errorMessage}}",
"updatedSublabelFailed": "Neuspješno ažuriranje podjezika: {{errorMessage}}",
"updatedLPRFailed": "Neuspješno ažuriranje tablice: {{errorMessage}}",
"updatedAttributesFailed": "Neuspješno ažuriranje atribute: {{errorMessage}}",
"audioTranscription": "Neuspješno zahtjev za audio transkripciju: {{errorMessage}}"
}
}
},
"label": "Oznaka",
"editSubLabel": {
"title": "Uredi podjeziku",
"desc": "Unesite novu podjeziku za ovaj {{label}}",
"descNoLabel": "Unesite novu podjeziku za ovaj pratiti objekt"
},
"editLPR": {
"title": "Uredi tablica",
"desc": "Unesite novu vrijednost tablice za ovaj {{label}}",
"descNoLabel": "Unesite novu vrijednost tablice za ovaj praćeni objekt"
},
"editAttributes": {
"title": "Uredi atribute",
"desc": "Odaberite atribute klasifikacije za ovaj {{label}}"
},
"snapshotScore": {
"label": "Snimak Rezultat"
},
"topScore": {
"label": "Najbolji Rezultat",
"info": "Najbolji rezultat je najviši srednji rezultat za praćeni objekt, pa se može razlikovati od rezultata prikazanog na minijaturi rezultata pretrage."
},
"score": {
"label": "Rezultat"
},
"recognizedLicensePlate": "Prepoznata tablica",
"attributes": "Atributi klasifikacije",
"estimatedSpeed": "Procijenjena brzina",
"objects": "Objekti",
"camera": "Kamera",
"zones": "Zone",
"timestamp": "Vremenski pečat",
"button": {
"findSimilar": "Pronađi slične",
"regenerate": {
"title": "Regeneriraj",
"label": "Regeneriraj opis praćenog objekta"
}
},
"description": {
"label": "Opis",
"placeholder": "Opis praćenog objekta",
"aiTips": "Frigate neće tražiti opis od vašeg generativnog AI provajdera dok se životni vijek praćenog objekta ne završi."
},
"expandRegenerationMenu": "Proširi izbornik regeneracije",
"regenerateFromSnapshot": "Regeneriraj iz snimka",
"regenerateFromThumbnails": "Regeneriraj iz minijatura",
"tips": {
"descriptionSaved": "Uspješno sačuvan opis",
"saveDescriptionFailed": "Neuspješno ažuriranje opisa: {{errorMessage}}"
},
"title": {
"label": "Naslov"
},
"scoreInfo": "Informacije o rezultatu"
},
"itemMenu": {
"downloadVideo": {
"label": "Preuzmi video",
"aria": "Preuzmi video"
},
"downloadSnapshot": {
"label": "Preuzmi snimak",
"aria": "Preuzmi snimak"
},
"downloadCleanSnapshot": {
"label": "Preuzmi čist snimak",
"aria": "Preuzmi čist snimak"
},
"viewTrackingDetails": {
"label": "Pregledaj detalje praćenja",
"aria": "Prikaži detalje praćenja"
},
"findSimilar": {
"label": "Pronađi slične",
"aria": "Pronađi slične praćene objekte"
},
"addTrigger": {
"label": "Dodaj izazov",
"aria": "Dodaj izazov za ovaj praćeni objekt"
},
"audioTranscription": {
"label": "Transkriptiraj",
"aria": "Zatraži transkripciju zvuka"
},
"submitToPlus": {
"label": "Pošalji na Frigate+",
"aria": "Pošalji na Frigate Plus"
},
"viewInHistory": {
"label": "Pregledajte u povijesti",
"aria": "Pregledajte u povijesti"
},
"deleteTrackedObject": {
"label": "Obriši ovaj praćeni objekt"
},
"showObjectDetails": {
"label": "Prikaži put objekta"
},
"hideObjectDetails": {
"label": "Sakrij put objekta"
},
"debugReplay": {
"label": "Debug ponovno snimanje",
"aria": "Pregledaj ovaj praćeni objekt u pogledu debug ponovnog snimanja"
},
"more": {
"aria": "Više"
}
},
"dialog": {
"confirmDelete": {
"title": "Potvrdi brisanje",
"desc": "Brisanje ovog praćenog objekta uklanja snimak, bilo kakve sačuvane ugradnje, i sve povezane unose detalja praćenja. Snimljeni materijal ovog praćenog objekta u pogledu povijesti <em>NEĆE</em> biti obrisan.<br /><br />Sigurno li želite nastaviti?"
},
"toast": {
"error": "Greška prilikom brisanja ovog praćenog objekta: {{errorMessage}}"
}
},
"noTrackedObjects": "Nijedan praćeni objekt nije pronađen",
"fetchingTrackedObjectsFailed": "Greška prilikom dohvaćanja praćenih objekata: {{errorMessage}}",
"trackedObjectsCount_one": "{{count}} praćeni objekt ",
"trackedObjectsCount_few": "{{count}} praćena objekta ",
"trackedObjectsCount_other": "{{count}} praćena objekta ",
"searchResult": {
"tooltip": "Pronađeno {{type}} na {{confidence}}%",
"previousTrackedObject": "Prethodni praćeni objekt",
"nextTrackedObject": "Sljedeći praćeni objekt",
"deleteTrackedObject": {
"toast": {
"success": "Praćeni objekt je uspješno obrisan.",
"error": "Neuspješno brisanje praćenog objekta: {{errorMessage}}"
}
}
},
"aiAnalysis": {
"title": "Analiza AI"
},
"concerns": {
"label": "Pitanja"
},
"objectLifecycle": {
"noImageFound": "Nije pronađena slika za ovaj praćeni objekt."
}
}
+128
View File
@@ -0,0 +1,128 @@
{
"search": "Pretraga",
"documentTitle": "Izvoz - Frigate",
"selected_one": "{{count}} odabrano",
"selected_other": "{{count}} odabrano",
"noExports": "Nijedan izvoz nije pronađen",
"headings": {
"cases": "Slučajevi",
"uncategorizedExports": "Nekategorizirani izvozi"
},
"deleteExport": {
"label": "Obriši izvoz",
"desc": "Da li ste sigurni da želite da obrišete {{exportName}}?"
},
"editExport": {
"title": "Preimenuj izvoz",
"desc": "Unesite novi naziv za ovaj izvoz.",
"saveExport": "Sačuvaj izvoz"
},
"tooltip": {
"shareExport": "Dijeli izvoz",
"downloadVideo": "Preuzmi video",
"editName": "Uredi naziv",
"deleteExport": "Obriši izvoz",
"assignToCase": "Dodaj u slučaj",
"removeFromCase": "Ukloni iz slučaja"
},
"toolbar": {
"newCase": "Novi slučaj",
"addExport": "Dodaj izvoz",
"editCase": "Uredi slučaj",
"deleteCase": "Obriši slučaj"
},
"toast": {
"error": {
"renameExportFailed": "Neuspješno preimenovanje izvoza: {{errorMessage}}",
"assignCaseFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}",
"caseSaveFailed": "Neuspješno čuvanje slučaja: {{errorMessage}}",
"caseDeleteFailed": "Neuspješno brisanje slučaja: {{errorMessage}}"
}
},
"deleteCase": {
"label": "Obriši slučaj",
"desc": "Da li ste sigurni da želite da obrišete {{caseName}}?",
"descKeepExports": "Izvozi će ostati dostupni kao nekategorizirani izvozi.",
"descDeleteExports": "Svi izvozi u ovom slučaju trajno će biti obrisani.",
"deleteExports": "Takođe izbriši izvoze"
},
"caseDialog": {
"title": "Dodaj u slučaj",
"description": "Odaberite postojeći slučaj ili napravite novi.",
"selectLabel": "Slučaj",
"newCaseOption": "Napravite novi slučaj",
"nameLabel": "Ime slučaja",
"descriptionLabel": "Opis"
},
"caseCard": {
"emptyCase": "Nema još izvoza"
},
"jobCard": {
"defaultName": "{{camera}} izvoz",
"queued": "U redu",
"running": "Pokretanje",
"preparing": "Priprema",
"copying": "Kopiranje",
"encoding": "Kodiranje",
"encodingRetry": "Kodiranje (ponovi)",
"finalizing": "Završavanje"
},
"caseView": {
"noDescription": "Nema opisa",
"createdAt": "Kreirano {{value}}",
"exportCount_one": "1 izvoz",
"exportCount_other": "{{count}} izvozi",
"cameraCount_one": "1 kamera",
"cameraCount_other": "{{count}} kamere",
"showMore": "Prikaži više",
"showLess": "Prikaži manje",
"emptyTitle": "Ovaj slučaj je prazan",
"emptyDescription": "Dodaj postojet će nekategorizirane izvoze kako bi slučaj ostao organizovan.",
"emptyDescriptionNoExports": "Nema dostupnih nekategoriziranih izvoza koje je moguće dodati još."
},
"caseEditor": {
"createTitle": "Kreiraj slučaj",
"editTitle": "Uredi slučaj",
"namePlaceholder": "Ime slučaja",
"descriptionPlaceholder": "Dodaj napomene ili kontekst za ovaj slučaj"
},
"addExportDialog": {
"title": "Dodaj izvoz u {{caseName}}",
"searchPlaceholder": "Pretraga nekategoriziranih izvoza",
"empty": "Nema nekategoriziranih izvoza koji odgovaraju ovoj pretrazi.",
"addButton_one": "Dodaj 1 izvoz",
"addButton_other": "Dodaj {{count}} izvoza",
"adding": "Dodavanje..."
},
"bulkActions": {
"addToCase": "Dodaj u slučaj",
"moveToCase": "Premjesti u slučaj",
"removeFromCase": "Ukloni iz slučaja",
"delete": "Obriši",
"deleteNow": "Obriši sada"
},
"bulkDelete": {
"title": "Obriši izvoze",
"desc_one": "Sigurni li ste da želite obrisati {{count}} izvoz?",
"desc_other": "Sigurni li ste da želite obrisati {{count}} izvoze?"
},
"bulkRemoveFromCase": {
"title": "Ukloni iz slučaja",
"desc_one": "Ukloni {{count}} izvoz iz ovog slučaja?",
"desc_other": "Ukloni {{count}} izvoze iz ovog slučaja?",
"descKeepExports": "Izvozi će biti premješteni u nekategorizirane.",
"descDeleteExports": "Izvozi će biti trajno obrisani.",
"deleteExports": "Umjesto toga, obriši izvoze"
},
"bulkToast": {
"success": {
"delete": "Uspješno obrisani izvozi",
"reassign": "Uspješno ažurirana dodjela slučaja",
"remove": "Uspješno uklonjeni izvozi iz slučaja"
},
"error": {
"deleteFailed": "Neuspješno brisanje izvoza: {{errorMessage}}",
"reassignFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}"
}
}
}
@@ -0,0 +1,98 @@
{
"description": {
"addFace": "Dodajte novu kolekciju u Biblioteku lica prema učitavanju svoje prve slike.",
"placeholder": "Unesite ime za ovu kolekciju",
"invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte.",
"nameCannotContainHash": "Ime ne može sadržavati #."
},
"details": {
"unknown": "Nepoznato",
"timestamp": "Vremenski pečat",
"scoreInfo": "Ocjena je težinski prosjek svih ocjena lica, težinski određen prema veličini lica u svakoj slici."
},
"train": {
"titleShort": "Nedavno",
"title": "Najnovije prepoznavanja",
"aria": "Odaberite nedavna prepoznavanja",
"empty": "Nema nedavnih pokušaja prepoznavanja lica"
},
"documentTitle": "Biblioteka lica - Frigate",
"uploadFaceImage": {
"title": "Učitajte sliku lica",
"desc": "Učitajte sliku za skeniranje lica i uključite za {{pageToggle}}"
},
"collections": "Kolekcije",
"createFaceLibrary": {
"new": "Stvori novo lice",
"nextSteps": "Da biste izgradili čvrstu osnovu:<li>Koristite karticu Najnovije prepoznavanja da biste odabrali i trenirali se na slikama za svaku detektiranu osobu.</li><li>Fokusirajte se na slike iz pravog ugla za najbolje rezultate; izbjegavajte slike za treniranje koje prikazuju lica pod uglom.</li></ul>"
},
"steps": {
"faceName": "Unesite ime lica",
"uploadFace": "Učitajte sliku lica",
"nextSteps": "Sljedeći koraci",
"description": {
"uploadFace": "Učitajte sliku od {{name}} koja prikazuje njihovo lice iz pravog ugla. Slika ne mora biti izrezana samo na njihovo lice."
}
},
"deleteFaceLibrary": {
"title": "Izbrišite ime",
"desc": "Da li ste sigurni da želite izbrisati kolekciju {{name}}? Ovo će trajno izbrisati sva povezana lica."
},
"deleteFaceAttempts": {
"title": "Izbrišite lica",
"desc_one": "Da li ste sigurni da želite izbrisati {{count}} lice? Ova akcija ne može se poništiti.",
"desc_few": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti.",
"desc_other": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti."
},
"renameFace": {
"title": "Preimenujte lice",
"desc": "Unesite novo ime za {{name}}"
},
"button": {
"deleteFaceAttempts": "Izbrišite lica",
"addFace": "Dodaj lice",
"renameFace": "Preimenuj lice",
"deleteFace": "Obriši lice",
"uploadImage": "Prenesi sliku",
"reprocessFace": "Ponovno obradi lice"
},
"imageEntry": {
"validation": {
"selectImage": "Molimo izaberite datoteku slike."
},
"dropActive": "Pustite sliku ovdje…",
"dropInstructions": "Povucite i ispišite, zalijepite sliku ovdje ili kliknite za odabir",
"maxSize": "Maksimalna veličina: {{size}}MB"
},
"nofaces": "Nema dostupnih lica",
"trainFaceAs": "Obuči lice kao:",
"trainFace": "Obuči lice",
"reclassifyFaceAs": "Ponovno klasificiraj lice kao:",
"reclassifyFace": "Ponovno klasificiraj lice",
"toast": {
"success": {
"uploadedImage": "Uspješno prenesena slika.",
"addFaceLibrary": "{{name}} je uspješno dodan u biblioteku lica!",
"deletedFace_one": "Uspješno obrisano {{count}} lice.",
"deletedFace_few": "Uspješno obrisana {{count}} lica.",
"deletedFace_other": "Uspješno obrisana {{count}} lica.",
"deletedName_one": "{{count}} lice je uspješno obrisano.",
"deletedName_few": "{{count}} lica su uspješno obrisana.",
"deletedName_other": "{{count}} lica su uspješno obrisana.",
"renamedFace": "Uspješno preimenovan lice na {{name}}",
"trainedFace": "Uspješno obučeno lice.",
"reclassifiedFace": "Uspješno ponovno klasificirano lice.",
"updatedFaceScore": "Uspješno ažurirana ocjena lica na {{name}} ({{score}})."
},
"error": {
"uploadingImageFailed": "Nije uspješno prenijeti sliku: {{errorMessage}}",
"addFaceLibraryFailed": "Nije uspješno postaviti ime lica: {{errorMessage}}",
"deleteFaceFailed": "Neuspješno brisanje: {{errorMessage}}",
"deleteNameFailed": "Nije uspješno obrisati ime: {{errorMessage}}",
"renameFaceFailed": "Nije uspješno preimenovati lice: {{errorMessage}}",
"trainFailed": "Nije uspješno trenirati: {{errorMessage}}",
"reclassifyFailed": "Nije uspješno ponovno klasifikovati lice: {{errorMessage}}",
"updateFaceScoreFailed": "Nije uspješno ažurirati bodove lica: {{errorMessage}}"
}
}
}
+199
View File
@@ -0,0 +1,199 @@
{
"documentTitle": {
"default": "Uživo - Frigate",
"withCamera": "{{camera}} - Uživo - Frigate"
},
"lowBandwidthMode": "Nizopojasni režim",
"twoWayTalk": {
"enable": "Omogući dvostrani razgovor",
"disable": "Onemogući dvostrani razgovor"
},
"cameraAudio": {
"enable": "Omogući zvuk kamere",
"disable": "Onemogući zvuk kamere"
},
"ptz": {
"move": {
"clickMove": {
"label": "Kliknite unutar okvira da biste centrirali kameru",
"enable": "Omogući klik za pomak",
"enableWithZoom": "Omogući klik za pomak / povucite za uvećanje",
"disable": "Onemogući klik za pomak"
},
"left": {
"label": "Pomaknite PTZ kameru ulevo"
},
"up": {
"label": "Pomaknite PTZ kameru gore"
},
"down": {
"label": "Pomaknite PTZ kameru dolje"
},
"right": {
"label": "Pomaknite PTZ kameru udesno"
}
},
"zoom": {
"in": {
"label": "Uvećajte PTZ kameru"
},
"out": {
"label": "Umanjite PTZ kameru"
}
},
"focus": {
"in": {
"label": "Fokusirajte PTZ kameru unapred"
},
"out": {
"label": "Fokusirajte PTZ kameru unazad"
}
},
"frame": {
"center": {
"label": "Kliknite unutar okvira da biste centrirali PTZ kameru"
}
},
"presets": "Preseti PTZ kamere"
},
"camera": {
"enable": "Omogući kameru",
"disable": "Onemogući kameru"
},
"muteCameras": {
"enable": "Utišajte sve kamere",
"disable": "Ponovo uključite zvuk za sve kamere"
},
"detect": {
"enable": "Omogući detekciju",
"disable": "Onemogući detekciju"
},
"recording": {
"enable": "Omogući snimanje",
"disable": "Onemogući snimanje"
},
"snapshots": {
"enable": "Omogući snimke",
"disable": "Onemogući snimke"
},
"snapshot": {
"takeSnapshot": "Preuzmi trenutni snimak",
"noVideoSource": "Nema dostupnog video izvora za snimak.",
"captureFailed": "Neuspješno snimanje trenutnog snimka.",
"downloadStarted": "Preuzimanje trenutnog snimka započeto."
},
"audioDetect": {
"enable": "Omogući detekciju zvuka",
"disable": "Onemogući detekciju zvuka"
},
"transcription": {
"enable": "Omogući prepoznavanje zvuka uživo",
"disable": "Onemogući prepoznavanje zvuka uživo"
},
"autotracking": {
"enable": "Omogući automatsko praćenje",
"disable": "Onemogući automatsko praćenje"
},
"streamStats": {
"enable": "Prikaži statistiku prijenosa",
"disable": "Sakrij statistiku prijenosa"
},
"manualRecording": {
"title": "Na zahtjev",
"tips": "Preuzmi trenutni snimak ili pokreni ručni događaj na temelju postavki trajanja snimanja ove kamere.",
"playInBackground": {
"label": "Ponovno postavi stream",
"desc": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
},
"showStats": {
"label": "Prikaži statistiku",
"desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
},
"debugView": "Pregled za otklanjanje grešaka",
"start": "Počni snimanje na zahtjev",
"started": "Pokrenuto ručno snimanje na zahtjev.",
"failedToStart": "Neuspješno pokretanje ručnog snimanja na zahtjev.",
"recordDisabledTips": "Kako je snimanje onemogućeno ili ograničeno u konfiguraciji za ovu kameru, spremat će se samo snimak.",
"end": "Završi snimanje na zahtjev",
"ended": "Završeno ručno snimanje na zahtjev.",
"failedToEnd": "Neuspješno završavanje ručnog snimanja na zahtjev."
},
"streamingSettings": "Postavke streamanja",
"notifications": "Obavještenja",
"audio": "Audio",
"suspend": {
"forTime": "Pauziraj za: "
},
"stream": {
"title": "Tok",
"audio": {
"tips": {
"title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
},
"available": "Audio je dostupan za ovaj stream",
"unavailable": "Audio nije dostupan za ovaj stream"
},
"debug": {
"picker": "Izbor streama nije dostupan u režimu debuga. Pregled debuga uvijek koristi stream dodeljen ulozi detekcije."
},
"twoWayTalk": {
"tips": "Vaš uređaj mora podržavati funkciju, a WebRTC mora biti konfiguriran za dvosmernu komunikaciju.",
"available": "Dvosmerna komunikacija je dostupna za ovaj stream",
"unavailable": "Dvosmerna komunikacija nije dostupna za ovaj stream"
},
"lowBandwidth": {
"tips": "Živo prikazivanje je u režimu niske propusnosti zbog buferiranja ili grešaka u streamu.",
"resetStream": "Ponovno postavi stream"
},
"playInBackground": {
"label": "Ponovno postavi stream",
"tips": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
}
},
"cameraSettings": {
"title": "{{camera}} Postavke",
"cameraEnabled": "Kamera omogućena",
"objectDetection": "Detekcija objekata",
"recording": "Snimanje",
"snapshots": "Snimci",
"audioDetection": "Detekcija zvuka",
"transcription": "Transkripcija zvuka",
"autotracking": "Autotračenje"
},
"history": {
"label": "Prikaži povijesne snimke"
},
"effectiveRetainMode": {
"modes": {
"all": "Sve",
"motion": "Kretanje",
"active_objects": "Aktivni objekti"
}
},
"editLayout": {
"label": "Uredi raspored",
"group": {
"label": "Uredi grupu kamera"
},
"exitEdit": "Izađi iz uređivanja"
},
"noCameras": {
"title": "Nema konfiguriranih kamera",
"description": "Počnite tako što ćete povezati kameru s Frigate.",
"buttonText": "Dodaj kameru",
"restricted": {
"title": "Nema dostupnih kamera",
"description": "Nemate dozvolu za pregled bilo koje kamere u ovoj grupi."
},
"default": {
"title": "Nema konfiguriranih kamera",
"description": "Počnite tako što ćete povezati kameru s Frigate.",
"buttonText": "Dodaj kameru"
},
"group": {
"title": "Nema kamera u grupi",
"description": "Ova grupa kamera nema dodeljene ili omogućene kamere.",
"buttonText": "Upravljajte grupama"
}
}
}
@@ -0,0 +1,77 @@
{
"documentTitle": "Pretraga pokreta - Frigate",
"title": "Pretraga pokreta",
"description": "Nacrtaj poligon da biste definirali regiju interesa, a zatim navedite vremenski raspon za pretragu promjena pokreta unutar te regije.",
"selectCamera": "Pretraga pokreta učitava se",
"startSearch": "Počni pretragu",
"searchStarted": "Pretraga započeta",
"searchCancelled": "Pretraga otkazana",
"cancelSearch": "Otkaži",
"searching": "Pretraga u toku.",
"searchComplete": "Pretraga završena",
"noResultsYet": "Pokrenite pretragu da biste pronašli promjene pokreta u odabranoj regiji",
"noChangesFound": "Nisu otkrivene promjene piksela u odabranoj regiji",
"changesFound_one": "Pronađeno {{count}} promjena pokreta",
"changesFound_few": "Pronađeno {{count}} nekoliko pokreta",
"changesFound_other": "Pronađeno {{count}} promjene pokreta",
"framesProcessed": "{{count}} okvir procesiran",
"jumpToTime": "Preskoči na ovo vrijeme",
"results": "Rezultati",
"showSegmentHeatmap": "Top mapa",
"newSearch": "Nova pretraga",
"clearResults": "Očisti rezultate",
"clearROI": "Očisti poligon",
"polygonControls": {
"points_one": "{{count}} tačka",
"points_few": "{{count}} tačke",
"points_other": "{{count}} tačke",
"undo": "Poništi posljednju tačku",
"reset": "Ponovi poligon"
},
"motionHeatmapLabel": "Top mapa pokreta",
"dialog": {
"title": "Pretraga pokreta",
"cameraLabel": "Kamera",
"previewAlt": "Pregled kamere za {{camera}}"
},
"timeRange": {
"title": "Opseg pretrage",
"start": "Početno vrijeme",
"end": "Krajnje vrijeme"
},
"settings": {
"title": "Postavke pretrage",
"parallelMode": "Paralelni način",
"parallelModeDesc": "Skeniranje više segmenata snimaka istovremeno (brže, ali značajno intenzivnije za CPU)",
"threshold": "Praga osjetljivosti",
"thresholdDesc": "Niže vrijednosti detektiraju manje promjene (1-255)",
"minArea": "Minimalna površina promjene",
"minAreaDesc": "Minimalni postotak područja interesa koji mora promijeniti da bi se smatrao značajnim",
"frameSkip": "Preskoči okvir",
"frameSkipDesc": "Obrađujte svaki N-ti okvir. Postavite ovo na brzinu okvira vaše kamere da biste obradili jedan okvir po sekundi (npr. 5 za 5 FPS kameru, 30 za 30 FPS kameru). Više vrijednosti će biti brže, ali mogu propustiti kratke događaje pokreta.",
"maxResults": "Maksimalni rezultati",
"maxResultsDesc": "Zaustavi nakon ovog broja odgovarajućih vremenskih oznaka"
},
"errors": {
"noCamera": "Molimo odaberite kameru",
"noROI": "Molimo nacrtajte područje interesa",
"noTimeRange": "Molimo odaberite vremenski opseg",
"invalidTimeRange": "Krajnje vrijeme mora biti nakon početnog vremena",
"searchFailed": "Pretraga neuspješna: {{message}}",
"polygonTooSmall": "Poligon mora imati najmanje 3 točke",
"unknown": "Nepoznata greška"
},
"changePercentage": "{{percentage}}% promijenjeno",
"metrics": {
"title": "Metrike pretrage",
"segmentsScanned": "Skenirani segmenti",
"segmentsProcessed": "Obrađeno",
"segmentsSkippedInactive": "Preskočeno (bez aktivnosti)",
"segmentsSkippedHeatmap": "Preskočeno (bez preklapanja ROI)",
"fallbackFullRange": "Povratni put skeniranje cijelog opsega",
"framesDecoded": "Dekodirani okviri",
"wallTime": "Vrijeme pretrage",
"segmentErrors": "Greške segmenta",
"seconds": "{{seconds}}s"
}
}
@@ -0,0 +1,12 @@
{
"filter": "Filtar",
"export": "Izvoz",
"calendar": "Kalendar",
"filters": "Filtari",
"toast": {
"error": {
"noValidTimeSelected": "Nije odabran valjan vremenski opseg",
"endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena"
}
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"title": "Debug ponavljanje",
"description": "Ponovno prikazivanje snimaka kamere za ispitivanje. Lista objekata prikazuje zakasnjelje sažetak detektiranih objekata, a kartica Zapisi prikazuje tok unutrašnjih poruka Frigate iz snimaka ponavljanja.",
"websocket_messages": "Poruke",
"dialog": {
"title": "Počni debug ponavljanje",
"description": "Kreiraj privremenu kameru za ponavljanje koja ponavlja povijesne snimke za ispitivanje problema detekcije i praćenja objekata. Kamera za ponavljanje će imati istu konfiguraciju detekcije kao i izvorna kamera. Odaberite vremenski raspon za početak.",
"camera": "Izvorna kamera",
"timeRange": "Vremenski opseg",
"preset": {
"1m": "Posljednja 1 minuta",
"5m": "Posljednje 5 minuta",
"timeline": "Iz vremenske linije",
"custom": "Prilagođeno"
},
"startButton": "Počni ponavljanje",
"selectFromTimeline": "Odaberite",
"starting": "Pokretanje ponavljanja...",
"startLabel": "Početak",
"endLabel": "Kraj",
"toast": {
"error": "Neuspješno pokretanje debug ponavljanja: {{error}}",
"alreadyActive": "Već postoji aktivna sesija ponavljanja",
"stopError": "Neuspješno zaustavljanje debug ponavljanja: {{error}}",
"goToReplay": "Idi na ponavljanje"
}
},
"page": {
"noSession": "Nema aktivne sesije ponavljanja",
"noSessionDesc": "Pokrenite debug ponavljanje iz pogleda Povijest klikom na dugme Debug Replay u alatnoj traci.",
"goToRecordings": "Idi na povijest",
"sourceCamera": "Izvorna kamera",
"replayCamera": "Kamera za ponavljanje",
"initializingReplay": "Inicijalizacija ponavljanja...",
"stoppingReplay": "Zaustavljanje ponavljanja...",
"stopReplay": "Zaustavi ponavljanje",
"confirmStop": {
"title": "Zaustavi režim ponavljanja za debagovanje?",
"description": "Ovo će zaustaviti sesiju ponavljanja i očistiti sve privremene podatke. Sigurni li?",
"confirm": "Zaustavi ponavljanje",
"cancel": "Otkaži"
},
"activity": "Aktivnost",
"objects": "Popis objekata",
"audioDetections": "Audio detekcije",
"noActivity": "Nema detektovane aktivnosti",
"activeTracking": "Aktivno praćenje",
"noActiveTracking": "Nema aktivnog praćenja",
"configuration": "Konfiguracija",
"configurationDesc": "Podesiti precizno detekciju pokreta i praćenje objekata za kameru za debagovanje ponavljanja. Promjene se ne čuvaju u datoteci konfiguracije Frigate.",
"preparingClip": "Pripremam klip…",
"preparingClipDesc": "Frigate spaja snimke za odabrani vremenski raspon. Ovo može potrajati minut za duže raspone.",
"startingCamera": "Pokretanje ponovnog pokretanja otklanjanja grešaka…",
"startError": {
"title": "Neuspjelo pokretanje ponovnog prikaza otklanjanja grešaka",
"back": "Povratak na historiju"
}
}
}
+73
View File
@@ -0,0 +1,73 @@
{
"search": "Pretraga",
"button": {
"save": "Sačuvaj pretragu",
"clear": "Očisti pretragu",
"delete": "Obriši sačuvanu pretragu",
"filterInformation": "Filtrirajte informacije",
"filterActive": "Filtari aktivni"
},
"savedSearches": "Sačuvane pretrage",
"searchFor": "Pretraga za {{inputValue}}",
"trackedObjectId": "ID praćenog objekta",
"filter": {
"label": {
"cameras": "Kamere",
"labels": "Oznake",
"zones": "Zone",
"sub_labels": "Podoznake",
"attributes": "Atributi",
"search_type": "Tip pretrage",
"time_range": "Vremenski opseg",
"before": "Prije",
"after": "Nakon",
"min_score": "Min. bodovi",
"max_score": "Max. bodovi",
"min_speed": "Min. brzina",
"max_speed": "Max. brzina",
"recognized_license_plate": "Prepoznata tablica",
"has_clip": "Ima klip",
"has_snapshot": "Ima snimak"
},
"searchType": {
"thumbnail": "Minijatura",
"description": "Opis"
},
"toast": {
"error": {
"beforeDateBeLaterAfter": "Datum 'before' mora biti kasniji od datuma 'after'.",
"afterDatebeEarlierBefore": "Datum 'after' mora biti raniji od datuma 'before'.",
"minScoreMustBeLessOrEqualMaxScore": "Vrijednost 'min_score' mora biti manja ili jednaka vrijednosti 'max_score'.",
"maxScoreMustBeGreaterOrEqualMinScore": "Vrijednost 'max_score' mora biti veća ili jednaka vrijednosti 'min_score'.",
"minSpeedMustBeLessOrEqualMaxSpeed": "Vrijednost 'min_speed' mora biti manja ili jednaka vrijednosti 'max_speed'.",
"maxSpeedMustBeGreaterOrEqualMinSpeed": "Vrijednost 'max_speed' mora biti veća ili jednaka vrijednosti 'min_speed'."
}
},
"tips": {
"title": "Kako koristiti tekstualne filtere",
"desc": {
"text": "Filteri vam pomažu da sužite rezultate pretrage. Evo kako ih koristiti u polju za unos:",
"step1": "Unesite ime ključa filtera, zatim dvojtočku (npr. \"kamere:\").",
"step2": "Izaberite vrijednost iz predloga ili unesite vlastitu.",
"step3": "Koristite više filtera dodavanjem jednog za drugim s razmakom između.",
"step4": "Filteri datuma (pre: i nakon:) koriste {{DateFormat}} format.",
"step5": "Filter raspona vremena koristi format {{exampleTime}}.",
"step6": "Uklonite filtre klikom na 'x' pored njih.",
"exampleLabel": "Primjer:"
}
},
"header": {
"currentFilterType": "Vrijednosti filtera",
"noFilters": "Filtari",
"activeFilters": "Aktivni filteri"
}
},
"similaritySearch": {
"title": "Pretraga sličnosti",
"active": "Pretraga sličnosti aktivna",
"clear": "Očisti pretragu sličnosti"
},
"placeholder": {
"search": "Pretraži…"
}
}
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
{
"documentTitle": {
"cameras": "Statistika kamere - Frigate",
"storage": "Statistika skladišta - Frigate",
"general": "Opća statistika - Frigate",
"enrichments": "Statistika bogatstva - Frigate",
"logs": {
"frigate": "Zapisi Frigate - Frigate",
"go2rtc": "Zapisi Go2RTC - Frigate",
"nginx": "Zapisi Nginx - Frigate",
"websocket": "Zapisi poruka - Frigate"
}
},
"title": "Sistem",
"metrics": "Sistem metrike",
"logs": {
"websocket": {
"label": "Zapisi",
"pause": "Pauziraj",
"resume": "Nastavi",
"clear": "Očisti",
"filter": {
"all": "Svi temi",
"topics": "Teme",
"events": "Događaji",
"reviews": "Pregledi",
"classification": "Klasifikacija",
"face_recognition": "Prepoznavanje lica",
"lpr": "LPR",
"camera_activity": "Aktivnost kamere",
"system": "Sistem",
"camera": "Kamera",
"all_cameras": "Sve kamere",
"cameras_count_one": "{{count}} Kamera",
"cameras_count_other": "{{count}} Kamere"
},
"empty": "Nema još prihvaćenih poruka",
"count_one": "{{count}} poruka",
"count_other": "{{count}} poruke",
"expanded": {
"payload": "Opterećenje"
}
},
"download": {
"label": "Preuzimanje zapisa"
},
"copy": {
"label": "Kopiraj u clipboard",
"success": "Zapisi su kopirani u clipboard",
"error": "Nije moguće kopirati zapise u clipboard"
},
"type": {
"label": "Tip",
"timestamp": "Vremenski pečat",
"tag": "Oznaka",
"message": "Poruka"
},
"tips": "Zapisi se prenose sa servera",
"toast": {
"error": {
"fetchingLogsFailed": "Greška prilikom preuzimanja zapisa: {{errorMessage}}",
"whileStreamingLogs": "Greška prilikom prijenosa protokola: {{errorMessage}}"
}
}
},
"general": {
"title": "Općenito",
"detector": {
"title": "Detektori",
"inferenceSpeed": "Brzina zaključivanja detektora",
"temperature": "Temperatura detektora",
"cpuUsage": "Korištenje CPU detektora",
"cpuUsageInformation": "CPU korištena za pripremu ulaznih i izlaznih podataka za/iz modela detekcije. Ova vrijednost ne mjeri korištenje zaključivanja, čak i ako se koristi GPU ili ubrzivač.",
"memoryUsage": "Korištenje memorije detektora"
},
"hardwareInfo": {
"title": "Hardverske informacije",
"gpuUsage": "Korištenje GPU",
"gpuMemory": "Memorija GPU",
"gpuEncoder": "Kodiralo GPU",
"gpuCompute": "GPU Izračunavanje / Kodiranje",
"gpuDecoder": "Dekodiranje GPU",
"gpuTemperature": "Temperatura GPU",
"gpuInfo": {
"vainfoOutput": {
"title": "Vainfo Izlaz",
"returnCode": "Kod povratka: {{code}}",
"processOutput": "Izlaz procesa:",
"processError": "Greška procesa:"
},
"nvidiaSMIOutput": {
"title": "Nvidia SMI Izlaz",
"name": "Ime: {{name}}",
"driver": "Vozač: {{driver}}",
"cudaComputerCapability": "CUDA sposobnost izračunavanja: {{cuda_compute}}",
"vbios": "VBios informacije: {{vbios}}"
},
"closeInfo": {
"label": "Zatvori informacije GPU"
},
"copyInfo": {
"label": "Kopiraj informacije GPU"
},
"toast": {
"success": "Kopirano informacije GPU u međuspremnik"
}
},
"npuUsage": "Korišćenje NPU",
"npuMemory": "Memorija NPU",
"npuTemperature": "Temperatura NPU",
"intelGpuWarning": {
"title": "Upozorenje o statistikama Intel GPU",
"message": "Statistike GPU nedostupne",
"description": "Ovo je poznati bug u alatima za prikaz statistika Intel GPU (intel_gpu_top) gdje će se prekiniti i ponovo vratiti GPU korišćenje od 0% čak i u slučajevima kada se hardverska akceleracija i detekcija objekata ispravno izvršavaju na (i)GPU. Ovo nije bug Frigate. Možete ponovo pokrenuti host kako biste privremeno popravili problem i potvrdili da GPU radi ispravno. Ovo ne utiče na performanse."
}
},
"otherProcesses": {
"title": "Drugi procesi",
"processCpuUsage": "Korišćenje CPU procesa",
"processMemoryUsage": "Korišćenje memorije procesa",
"series": {
"go2rtc": "go2rtc",
"recording": "Snimanje",
"review_segment": "pregled segmenta",
"embeddings": "Ugrađivanja",
"audio_detector": "audio detektor"
}
}
},
"storage": {
"title": "Skladište",
"overview": "Pregled",
"recordings": {
"title": "Snimci",
"tips": "Ova vrijednost predstavlja ukupno skladište koje se koristi za snimke u bazi podataka Frigate. Frigate ne praćenje korišćenje skladišta za sve datoteke na vašem disku.",
"earliestRecording": "Najstariji dostupni snimak:"
},
"shm": {
"title": "Alokacija SHM (deljenja memorije)",
"warning": "Trenutna veličina SHM od {{total}}MB je prevelika. Povećajte je na najmanje {{min_shm}}MB.",
"frameLifetime": {
"title": "Vijek trajanja okvira",
"description": "Svaka kamera ima {{frames}} slotova za okvire u deljenoj memoriji. Na najbržoj brzini okvira kamere, svaki okvir je dostupan za približno {{lifetime}}s prije nego što se prepiše."
}
},
"cameraStorage": {
"title": "Skladište kamere",
"camera": "Kamera",
"unusedStorageInformation": "Informacije o neiskorišćenom skladištu",
"storageUsed": "Skladište",
"percentageOfTotalUsed": "Postotak ukupno",
"bandwidth": "Širina pojasa",
"unused": {
"title": "Neiskorišćeno",
"tips": "Ova vrijednost može nepravilno predstavljati slobodno prostor dostupan Frigate ako imate druge datoteke pohranjene na vašem disku izvan snimaka Frigate. Frigate ne praćenje korišćenje skladišta izvan svojih snimaka."
}
}
},
"cameras": {
"title": "Kamere",
"overview": "Pregled",
"info": {
"aspectRatio": "odnos stranica",
"cameraProbeInfo": "{{camera}} Informacije o ispitivanju kamere",
"streamDataFromFFPROBE": "Podaci o prijenosu se dobijaju pomoću <code>ffprobe</code>.",
"fetching": "Prenošenje podataka o kameri",
"stream": "Prijenos {{idx}}",
"video": "Video:",
"codec": "Kodek:",
"resolution": "Rješenje:",
"fps": "FPS:",
"unknown": "Nepoznato",
"audio": "Zvuk:",
"error": "Greška: {{error}}",
"tips": {
"title": "Informacije o ispitivanju kamere"
}
},
"framesAndDetections": "Okviri / Detekcije",
"label": {
"camera": "Kamera",
"detect": "detektirati",
"skipped": "preskočeno",
"ffmpeg": "FFmpeg",
"capture": "snimiti",
"overallFramesPerSecond": "ukupni okviri po sekundi",
"overallDetectionsPerSecond": "ukupne detekcije po sekundi",
"overallSkippedDetectionsPerSecond": "ukupno preskočene detekcije po sekundi",
"cameraFfmpeg": "{{camName}} FFmpeg",
"cameraCapture": "{{camName}} snimiti",
"cameraDetect": "{{camName}} detektirati",
"cameraGpu": "{{camName}} GPU",
"cameraFramesPerSecond": "{{camName}} okviri po sekundi",
"cameraDetectionsPerSecond": "{{camName}} detekcije po sekundi",
"cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih detekcija u sekundi"
},
"connectionQuality": {
"title": "Kvaliteta veze",
"excellent": "Izuzetno dobra",
"fair": "Uredna",
"poor": "Loša",
"unusable": "Nepogodna",
"fps": "FPS",
"expectedFps": "Očekivani FPS",
"reconnectsLastHour": "Ponovne povezivanja (posljednje satu)",
"stallsLastHour": "Pauze (posljednje satu)"
},
"toast": {
"success": {
"copyToClipboard": "Podaci o testiranju kopirani u clipboard."
},
"error": {
"unableToProbeCamera": "Nemoguće testiranje kamere: {{errorMessage}}"
}
}
},
"lastRefreshed": "Posljednje ažuriranje: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za detekciju ({{detectAvg}}%)",
"healthy": "Sistem je zdrav",
"reindexingEmbeddings": "Ponovno indeksiranje ugrađenih vjerodajnica ({{processed}}% završeno)",
"cameraIsOffline": "{{camera}} je offline",
"detectIsSlow": "{{detect}} je spor ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} je vrlo spor ({{speed}} ms)",
"shmTooLow": "/dev/shm alokacija ({{total}} MB) treba povećati na najmanje {{min}} MB.",
"debugReplayActive": "Debug ponavljanje sesije je aktivno"
},
"enrichments": {
"title": "Obogaćivanja",
"infPerSecond": "Inferencije po sekundi",
"averageInf": "Prosjek vremena inferencije",
"embeddings": {
"image_embedding": "Slika ugrađenih vjerodajnica",
"text_embedding": "Tekst ugrađenih vjerodajnica",
"face_recognition": "Prepoznavanje lica",
"plate_recognition": "Prepoznavanje ploča",
"image_embedding_speed": "Brzina ugradnje slika",
"face_embedding_speed": "Brzina ugradnje lica",
"face_recognition_speed": "Brzina prepoznavanja lica",
"plate_recognition_speed": "Brzina prepoznavanja ploča",
"text_embedding_speed": "Brzina ugradnje teksta",
"yolov9_plate_detection_speed": "Brzina detekcije ploča YOLOv9",
"yolov9_plate_detection": "Detekcija ploča YOLOv9",
"review_description": "Pregled opisa",
"review_description_speed": "Brzina pregleda opisa",
"review_description_events_per_second": "Pregled opisa",
"object_description": "Opis objekta",
"object_description_speed": "Brzina opisa objekta",
"object_description_events_per_second": "Opis objekta",
"classification": "{{name}} Klasifikacija",
"classification_speed": "{{name}} Brzina klasifikacije",
"classification_events_per_second": "{{name}} Događaji klasifikacije po sekundi"
}
}
}
+3 -2
View File
@@ -109,7 +109,8 @@
"classification": "Classificació",
"chat": "Xat",
"actions": "Accions",
"profiles": "Perfils"
"profiles": "Perfils",
"features": "Característiques"
},
"pagination": {
"previous": {
@@ -241,7 +242,7 @@
"done": "Fet",
"disabled": "Deshabilitat",
"disable": "Deshabilitar",
"save": "Guardar",
"save": "Desa",
"copy": "Copiar",
"back": "Enrere",
"pictureInPicture": "Imatge en Imatge",
+72 -3
View File
@@ -60,15 +60,76 @@
"noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid",
"failed": "No s'ha pogut inciar l'exportació: {{error}}"
},
"view": "Vista"
"view": "Vista",
"queued": "Exporta a la cua. Mostra el progrés a la pàgina d'exportacions.",
"batchSuccess_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.",
"batchSuccess_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
"batchSuccess_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
"batchPartial": "S'han iniciat {{successful}} de {{total}} exportacions. Càmeres fallides: {{failedCameras}}",
"batchFailed": "No s'han pogut iniciar {{total}} exportacions. Càmeres fallides: {{failedCameras}}",
"batchQueuedSuccess_one": "Exporta a la cua 1. Obrint el cas ara.",
"batchQueuedSuccess_many": "{{count}} exportacions a la cua. Obrint el cas ara.",
"batchQueuedSuccess_other": "{{count}} exportacions a la cua. Obrint el cas ara.",
"batchQueuedPartial": "{{successful}} de {{total}} exportacions a la cua. Càmeres fallides: {{failedCameras}}",
"batchQueueFailed": "No s'han pogut posar a la cua {{total}} exportacions. Càmeres fallides: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Guardar exportació",
"previewExport": "Previsualitzar exportació"
"previewExport": "Previsualitzar exportació",
"queueingExport": "S'està fent la cua de l'exportació...",
"useThisRange": "Utilitza aquest interval"
},
"case": {
"label": "Cas",
"placeholder": "Selecciona un cas"
"placeholder": "Selecciona un cas",
"newCaseOption": "Crea un cas no",
"newCaseNamePlaceholder": "Nom de cas nou",
"newCaseDescriptionPlaceholder": "Descripció del cas",
"nonAdminHelp": "Es crearà un nou cas per a aquestes exportacions."
},
"queueing": "S'està fent la cua de l'exportació...",
"tabs": {
"export": "Càmera única",
"multiCamera": "Multicàmera"
},
"multiCamera": {
"timeRange": "Interval de temps",
"selectFromTimeline": "Selecciona des de la línia de temps",
"cameraSelection": "Càmeres",
"cameraSelectionHelp": "Les càmeres amb objectes rastrejats en aquest interval de temps estan preseleccionades",
"checkingActivity": "Comprovant l'activitat de la càmera...",
"noCameras": "No hi ha càmeres disponibles",
"detectionCount_one": "1 objecte rastrejat",
"detectionCount_many": "{{count}} objectes rastrejats",
"detectionCount_other": "{{count}} objectes rastrejats",
"nameLabel": "Nom de l'exportació",
"namePlaceholder": "Nom base opcional per a aquestes exportacions",
"queueingButton": "S'estan posant a la cua les exportacions...",
"exportButton_one": "Exporta 1 càmera",
"exportButton_many": "Exporta {{count}} càmeres",
"exportButton_other": "Exporta {{count}} càmeres"
},
"multi": {
"title_one": "Exporta {{count}} ressenyes",
"title_many": "Exporta {{count}} ressenyes",
"title_other": "Exporta {{count}} ressenyes",
"description": "Exporta cada revisió seleccionada. Totes les exportacions s'agruparan en un sol cas.",
"descriptionNoCase": "Exporta cada revisió seleccionada.",
"caseNamePlaceholder": "Exporta la revisió - {{date}}",
"exportButton_one": "Exporta {{count}} ressenyes",
"exportButton_many": "Exporta {{count}} ressenyes",
"exportButton_other": "Exporta {{count}} ressenyes",
"exportingButton": "S'està exportant...",
"toast": {
"started_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.",
"started_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
"started_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
"startedNoCase_one": "S'ha iniciat l'exportació 1.",
"startedNoCase_many": "S'han iniciat {{count}} exportacions.",
"startedNoCase_other": "S'han iniciat {{count}} exportacions.",
"partial": "S'han iniciat {{successful}} de {{total}} exportacions. Ha fallat: {{failedItems}}",
"failed": "No s'han pogut iniciar {{total}} exportacions. Ha fallat: {{failedItems}}"
}
}
},
"streaming": {
@@ -116,6 +177,14 @@
"success": "Els enregistraments de vídeo associats als elements de revisió seleccionats shan suprimit correctament.",
"error": "No s'ha pogut suprimir: {{error}}"
}
},
"shareTimestamp": {
"label": "Comparteix la marca horària",
"title": "Comparteix la marca horària",
"description": "Comparteix un URL amb marca horària de la posició actual del jugador o tria una marca horària personalitzada. Tingueu en compte que aquest no és un URL de compartició pública i només és accessible per als usuaris amb accés a Frigate i aquesta càmera.",
"custom": "Marca horària personalitzada",
"button": "Comparteix l'URL de la marca horària",
"shareTitle": "Marca de temps de revisió de Frigate: {{camera}}"
}
},
"imagePicker": {
+2 -1
View File
@@ -32,7 +32,8 @@
"noPreviewFoundFor": "No s'ha trobat cap previsualització per a {{cameraName}}",
"submitFrigatePlus": {
"title": "Enviar aquesta imatge a Frigate+?",
"submit": "Enviar"
"submit": "Enviar",
"previewError": "No s'ha pogut carregar la vista prèvia de la instantània. És possible que l'enregistrament no estigui disponible en aquest moment."
},
"livePlayerRequiredIOSVersion": "Es requereix iOS 17.1 o superior per a aquest tipus de reproducció en directe.",
"streamOffline": {
+5 -1
View File
@@ -13,7 +13,7 @@
"description": "Habilitat"
},
"audio": {
"label": "Esdeveniments d'àudio",
"label": "Detecció d'àudio",
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a aquesta càmera.",
"enabled": {
"label": "Habilita la detecció d'àudio",
@@ -485,6 +485,10 @@
"hwaccel_args": {
"label": "Exporta els arguments de l'hwaccel",
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
},
"max_concurrent": {
"label": "Màxim d'exportacions concurrents",
"description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
}
},
"preview": {
+8 -4
View File
@@ -341,6 +341,10 @@
"hwaccel_args": {
"label": "Exporta els arguments de l'hwaccel",
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
},
"max_concurrent": {
"label": "Màxim d'exportacions concurrents",
"description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
}
},
"preview": {
@@ -975,8 +979,8 @@
"description": "Habilita el monitoratge d'amplada de banda per procés per als processos i detectors de ffmpeg de càmera (requereix capacitats)."
},
"intel_gpu_device": {
"label": "Dispositiu SR-IOV",
"description": "Identificador de dispositiu utilitzat quan es tracten les GPU d'Intel com a SR-IOV per corregir les estadístiques de GPU."
"label": "Dispositiu GPU d'Intel",
"description": "Adreça de bus PCI o camí del dispositiu DRM (p. ex. /dev/dri/card1) utilitzat per fixar les estadístiques de GPU d'Intel a un dispositiu específic quan hi ha múltiples."
}
},
"version_check": {
@@ -1951,7 +1955,7 @@
},
"roles": {
"label": "Rols",
"description": "Funcions genAI (eines, visió, incrustacions); un proveïdor per rol."
"description": "Rols de GenAI (xat, descripcions, incrustacions); un proveïdor per rol."
},
"provider_options": {
"label": "Opcions del proveïdor",
@@ -1963,7 +1967,7 @@
}
},
"audio": {
"label": "Esdeveniments d'àudio",
"label": "Detecció d'àudio",
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a totes les càmeres; es pot substituir per càmera.",
"enabled": {
"label": "Habilita la detecció d'àudio",
+46
View File
@@ -0,0 +1,46 @@
{
"documentTitle": "Xat - Frigate",
"title": "Xat Frigate",
"subtitle": "El teu assistent d'AI per a gestionar càmeres i coneixements",
"placeholder": "Pregunta qualsevol cosa...",
"error": "Alguna cosa ha fallat. Torna-ho a provar.",
"processing": "Processant...",
"toolsUsed": "Usades: {{tools}}",
"showTools": "Mostra eines ({{count}})",
"hideTools": "Amaga eines",
"call": "Truca",
"result": "Resultat",
"arguments": "Variables:",
"response": "Resposta:",
"attachment_chip_label": "{{label}} a {{camera}}",
"attachment_chip_remove": "Elimina l'adjunt",
"open_in_explore": "Obre en l'explorador",
"attach_event_aria": "Adjunta l'esdeveniment {{eventId}}",
"attachment_picker_paste_label": "O enganxa l'ID de l'esdeveniment",
"attachment_picker_attach": "Adjunta",
"attachment_picker_placeholder": "Adjunta un esdeveniment",
"quick_reply_find_similar": "Troba albiraments similars",
"quick_reply_tell_me_more": "Explica'm més sobre això",
"quick_reply_when_else": "Quan més es va veure?",
"quick_reply_find_similar_text": "Troba albiraments similars a això.",
"quick_reply_tell_me_more_text": "Parla'm més d'aquest.",
"quick_reply_when_else_text": "Quan més es va veure això?",
"anchor": "Referència",
"similarity_score": "Similitud",
"no_similar_objects_found": "No s'ha trobat cap objecte similar.",
"semantic_search_required": "La cerca semàntica ha d'estar habilitada per trobar objectes similars.",
"send": "Envia",
"suggested_requests": "Proveu de preguntar:",
"starting_requests": {
"show_recent_events": "Mostra els esdeveniments recents",
"show_camera_status": "Mostra l'estat de la càmera",
"recap": "Què va passar mentre jo era fora?",
"watch_camera": "Observa una càmera per a l'activitat"
},
"starting_requests_prompts": {
"show_recent_events": "Mostra'm els esdeveniments recents de l'última hora",
"show_camera_status": "Quin és l'estat actual de les meves càmeres?",
"recap": "Què va passar mentre jo era fora?",
"watch_camera": "Vigila la porta d'entrada i fes-me saber si algú apareix"
}
}
+3 -1
View File
@@ -27,7 +27,9 @@
},
"documentTitle": "Revisió - Frigate",
"recordings": {
"documentTitle": "Enregistraments - Frigate"
"documentTitle": "Enregistraments - Frigate",
"invalidSharedLink": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'un error d'anàlisi.",
"invalidSharedCamera": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'una càmera desconeguda o no autoritzada."
},
"calendarFilter": {
"last24Hours": "Últimes 24 hores"
+5 -2
View File
@@ -248,7 +248,7 @@
"dialog": {
"confirmDelete": {
"title": "Confirmar la supressió",
"desc": "Eliminant aquest objecte seguit borrarà l'snapshot, qualsevol embedding gravat, i qualsevol detall de seguiment. Les imatges gravades d'aquest objecte seguit en l'historial <em>NO</em> seràn eliminades.<br /><br />Estas segur que vols continuar?"
"desc": "Suprimir aquest objecte rastrejat elimina la instantània, qualsevol incrustació desada, i qualsevol entrada de detalls de seguiment associada. Les imatges gravades d'aquest objecte seguit en l'historial <em>NO</em> seràn eliminades.<br /><br />Estas segur que vols continuar?"
},
"toast": {
"error": "S'ha produït un error en suprimir aquest objecte rastrejat: {{errorMessage}}"
@@ -289,7 +289,10 @@
"zones": "Zones",
"ratio": "Ràtio",
"area": "Àrea",
"score": "Puntuació"
"score": "Puntuació",
"computedScore": "Puntuació calculada",
"topScore": "Puntuació superior",
"toggleAdvancedScores": "Commuta les puntuacions avançades"
}
},
"annotationSettings": {
+91 -2
View File
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Error al canviar el nom de lexportació: {{errorMessage}}",
"assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}"
"assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}",
"caseSaveFailed": "No s'ha pogut desar el cas: {{errorMessage}}",
"caseDeleteFailed": "No s'ha pogut suprimir el cas: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Baixa el vídeo",
"editName": "Edita el nom",
"deleteExport": "Suprimeix l'exportació",
"assignToCase": "Afegeix al cas"
"assignToCase": "Afegeix al cas",
"removeFromCase": "Elimina del cas"
},
"headings": {
"cases": "Casos",
@@ -35,5 +38,91 @@
"newCaseOption": "Crea un cas nou",
"nameLabel": "Nom del cas",
"descriptionLabel": "Descripció"
},
"toolbar": {
"newCase": "Cas nou",
"addExport": "Afegeix una exportació",
"editCase": "Edita el cas",
"deleteCase": "Suprimeix el cas"
},
"deleteCase": {
"label": "Suprimeix el cas",
"desc": "Esteu segur que voleu suprimir {{caseName}}?",
"descKeepExports": "Les exportacions continuaran estant disponibles com a exportacions sense categoria.",
"descDeleteExports": "Totes les exportacions en aquest cas s'eliminaran permanentment.",
"deleteExports": "Elimina també les exportacions"
},
"caseCard": {
"emptyCase": "Encara no hi ha exportacions"
},
"jobCard": {
"defaultName": "Exportació de {{camera}}",
"queued": "En cua",
"running": "En execució",
"preparing": "Preparant",
"copying": "Copiant",
"encoding": "Codificant",
"encodingRetry": "Codificant (reintent)",
"finalizing": "Finalitzant"
},
"caseView": {
"noDescription": "Sense descripció",
"createdAt": "{{value}} creat",
"exportCount_one": "1 exportació",
"exportCount_other": "{{count}} exportacions",
"cameraCount_one": "1 càmera",
"cameraCount_other": "{{count}} càmeres",
"showMore": "Mostra'n més",
"showLess": "Mostra menys",
"emptyTitle": "Aquest cas és buit",
"emptyDescription": "Afegeix les exportacions no categoritzades existents per mantenir el cas organitzat.",
"emptyDescriptionNoExports": "Encara no hi ha exportacions sense categoria per afegir."
},
"caseEditor": {
"createTitle": "Crea un cas",
"editTitle": "Edita el cas",
"namePlaceholder": "Nom del cas",
"descriptionPlaceholder": "Afegeix notes o context per a aquest cas"
},
"addExportDialog": {
"title": "Afegeix l'exportació a {{caseName}}",
"searchPlaceholder": "Cerca exportacions sense categoria",
"empty": "No hi ha exportacions sense categoria que coincideixin amb aquesta cerca.",
"addButton_one": "Afegeix 1 exportació",
"addButton_other": "Afegeix {{count}} exportacions",
"adding": "S'està afegint..."
},
"selected_one": "{{count}} seleccionats",
"selected_other": "{{count}} seleccionats",
"bulkActions": {
"addToCase": "Afegeix al cas",
"moveToCase": "Mou al cas",
"removeFromCase": "Elimina del cas",
"delete": "Suprimeix",
"deleteNow": "Suprimeix ara"
},
"bulkDelete": {
"title": "Suprimeix les exportacions",
"desc_one": "Esteu segur que voleu suprimir {{count}} l'exportació?",
"desc_other": "steu segur que voleu suprimir {{count}} exportacions?"
},
"bulkRemoveFromCase": {
"title": "Elimina del cas",
"desc_one": "Voleu suprimir {{count}} d'aquest cas?",
"desc_other": "Voleu eliminar {{count}} exportacions d'aquest cas?",
"descKeepExports": "Les exportacions es mouran a sense categoria.",
"descDeleteExports": "Les exportacions s'eliminaran permanentment.",
"deleteExports": "Suprimeix les exportacions"
},
"bulkToast": {
"success": {
"delete": "Exportacions suprimides amb èxit",
"reassign": "Assignació de cas actualitzada amb èxit",
"remove": "S'han eliminat les exportacions del cas"
},
"error": {
"deleteFailed": "No s'han pogut suprimir les exportacions: {{errorMessage}}",
"reassignFailed": "No s'ha pogut actualitzar l'assignació de cas: {{errorMessage}}"
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
"uploadFace": "Puja una imatge del rostre",
"nextSteps": "Següents passos",
"description": {
"uploadFace": "Puja una imatge de {{name}} que mostri el seu rostre de cares. No cal que la imatge estigui retallada només al rostre."
"uploadFace": "Pugeu una imatge de {{name}} que mostra la seva cara des d'un angle frontal. La imatge no necessita ser retallada a la seva cara."
}
},
"selectFace": "Seleccionar rostre",
+2 -1
View File
@@ -70,7 +70,8 @@
},
"recording": {
"enable": "Habilitar gravació",
"disable": "Deshabilita l'enregistrament"
"disable": "Deshabilita l'enregistrament",
"disabledInConfig": "L'enregistrament primer s'ha d'habilitar a la configuració d'aquesta càmera."
},
"snapshots": {
"enable": "Habilita captura d'instantània",
@@ -0,0 +1,77 @@
{
"documentTitle": "Busca Deteccións - Frigate",
"title": "Búsqueda de Deteccions",
"selectCamera": "Búsqueda de Deteccions s'esta carregant",
"startSearch": "Començar Búsqueda",
"searchStarted": "Búsqueda inicada",
"searchCancelled": "Búsqueda cancel·lada",
"cancelSearch": "Cancel·lar",
"searching": "Búsqueda en progrés.",
"searchComplete": "Búsqueda completa",
"description": "Dibuixa un polígon per definir la regió d'interès, i especifica un interval de temps per cercar canvis de moviment dins d'aquesta regió.",
"noResultsYet": "Executa una cerca per a trobar canvis de moviment a la regió seleccionada",
"noChangesFound": "No s'ha detectat cap canvi de píxel a la regió seleccionada",
"changesFound_one": "S'ha trobat el canvi de moviment {{count}}",
"changesFound_many": "S'han trobat {{count}} canvis de moviment",
"changesFound_other": "S'han trobat {{count}} canvis de moviment",
"framesProcessed": "{{count}} fotogrames processats",
"jumpToTime": "Salta a aquesta hora",
"results": "Resultats",
"showSegmentHeatmap": "Mapa de calor",
"newSearch": "Cerca nova",
"clearResults": "Neteja els resultats",
"clearROI": "Neteja el polígon",
"polygonControls": {
"points_one": "{{count}} punt",
"points_many": "{{count}} punts",
"points_other": "{{count}} punts",
"undo": "Desfés l'últim punt",
"reset": "Restableix el polígon"
},
"motionHeatmapLabel": "Mapa de calor del moviment",
"dialog": {
"title": "Cerca de moviment",
"cameraLabel": "Càmara",
"previewAlt": "Vista prèvia de la càmera per a {{camera}}"
},
"timeRange": {
"title": "Interval de cerca",
"start": "Hora d'inici",
"end": "Hora final"
},
"settings": {
"title": "Configuració de la cerca",
"parallelMode": "Mode paral·lel",
"parallelModeDesc": "Escaneja múltiples segments d'enregistrament al mateix temps (més ràpid, però significativament més intensiu en CPU)",
"threshold": "Llindar de la sensibilitat",
"thresholdDesc": "Els valors més baixos detecten canvis més petits (1-255)",
"minArea": "Àrea de canvi mínim",
"minAreaDesc": "Percentatge mínim de la regió d'interès que s'ha de canviar per considerar-se significatiu",
"frameSkip": "Omet el fotograma",
"frameSkipDesc": "Processa cada N fotograma. Establiu això a la velocitat de fotogrames de la càmera per processar un fotograma per segon (p. ex. 5 per a una càmera de 5 FPS, 30 per a una càmera de 30 FPS). Els valors més alts seran més ràpids, però poden perdre els esdeveniments de curt moviment.",
"maxResults": "Resultats màxims",
"maxResultsDesc": "Atura després d'aquestes quantes marques horàries coincidents"
},
"errors": {
"noCamera": "Seleccioneu una càmera",
"noROI": "Dibuixeu una regió d'interès",
"noTimeRange": "Seleccioneu un interval de temps",
"invalidTimeRange": "L'hora de finalització ha de ser posterior a l'hora d'inici",
"searchFailed": "Ha fallat la cerca: {{message}}",
"polygonTooSmall": "El polígon ha de tenir almenys 3 punts",
"unknown": "Error desconegut"
},
"changePercentage": "{{percentage}}% canviat",
"metrics": {
"title": "Cerca les mètriques",
"segmentsScanned": "Segments escanejats",
"segmentsProcessed": "Processat",
"segmentsSkippedInactive": "S'ha omès (sense activitat)",
"segmentsSkippedHeatmap": "S'ha omès (sense superposició ROI)",
"fallbackFullRange": "Escaneig de rang complet alternatiu",
"framesDecoded": "Fotogrames descodificats",
"wallTime": "Temps de cerca",
"segmentErrors": "Errors del segment",
"seconds": "{{seconds}}s"
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"page": {
"startError": {
"back": "Torna a l'Historial",
"title": "No s'ha pogut iniciar la repetició de la depuració"
},
"sourceCamera": "Camera d'origen",
"replayCamera": "Reproduïr Càmera",
"initializingReplay": "Inicialitzant depurar repetició...",
"stoppingReplay": "Parant depurar repetició...",
"stopReplay": "Parar Repetició",
"confirmStop": {
"title": "Parar Depurar Repetició?",
"description": "Aixó pararà la sessió i netejarà les dades temporals. Estás segur?",
"confirm": "Parar Repetició",
"cancel": "Cancel·lar"
},
"activity": "Activitat",
"objects": "Llista d'Objectes",
"audioDetections": "Deteccions d'Audio",
"noActivity": "Sense activitat detectada",
"activeTracking": "Tracking Actiu",
"noActiveTracking": "Sense tracking actiu",
"configuration": "Configuració",
"configurationDesc": "Configuració d'ajust fi de detecció de moviment i tracking d'objectes per a la depuració de reproducció de càmera. Cap canvi es graba en el teu arxiu de configuració de Frigate.",
"noSession": "No hi ha una sessió activa de reproducció de depuració",
"noSessionDesc": "Inicia una reproducció de depuració des de la vista Historial fent clic al botó Accions a la barra d'eines i escollint Depura Repeteix.",
"goToRecordings": "Ves a l'historial",
"preparingClip": "S'està preparant el clip…",
"preparingClipDesc": "Frigate està cosint enregistraments per a l'interval de temps seleccionat. Això pot trigar un minut en intervals més llargs.",
"startingCamera": "S'està iniciant la repetició de la depuració…"
},
"title": "Repetició de depuració",
"websocket_messages": "Missatges",
"dialog": {
"title": "Iniciar Depuració de Repeticions",
"camera": "Càmera Font",
"timeRange": "Rang de Temps",
"preset": {
"1m": "Últim 1 Minut",
"5m": "Últims 5 Minuts",
"timeline": "Desde la Línia de Temps",
"custom": "Personalitzat"
},
"description": "Crea una càmera de reproducció temporal que fa bucles de metratge històric per depurar la detecció d'objectes i els problemes de seguiment. La càmera de reproducció tindrà la mateixa configuració de detecció que la càmera d'origen. Trieu un interval de temps per començar.",
"startButton": "Inicia la repetició",
"selectFromTimeline": "Selecciona",
"starting": "S'està iniciant la repetició...",
"startLabel": "Inici",
"endLabel": "Final",
"toast": {
"error": "No s'ha pogut iniciar la repetició de depuració: {{error}}",
"alreadyActive": "Ja hi ha activada una sessió de reproducció",
"stopError": "No s'ha pogut aturar la repetició de depuració: {{error}}",
"goToReplay": "Ves a la repetició"
}
},
"description": "Reprodueix els enregistraments de la càmera per a la depuració. La llista d'objectes mostra un resum retardat en el temps dels objectes detectats i la pestanya Missatges mostra un flux de missatges interns de la fragata a partir del metratge de reproducció."
}
+61 -12
View File
@@ -43,7 +43,7 @@
"globalMotion": "Detecció de moviment",
"globalObjects": "Objectes",
"globalReview": "Revisió",
"globalAudioEvents": "Esdeveniments d'àudio",
"globalAudioEvents": "Detecció d'àudio",
"globalLivePlayback": "Reproducció en directe",
"globalTimestampStyle": "Estil de la marca horària",
"systemDatabase": "Base de dades",
@@ -73,7 +73,7 @@
"cameraMotion": "Detecció de moviment",
"cameraObjects": "Objectes",
"cameraConfigReview": "Revisió",
"cameraAudioEvents": "Esdeveniments d'àudio",
"cameraAudioEvents": "Detecció d'àudio",
"cameraAudioTranscription": "Transcripció d'àudio",
"cameraNotifications": "Notificacions",
"cameraLivePlayback": "Reproducció en directe",
@@ -1280,7 +1280,8 @@
},
"hikvision": {
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
}
},
"resolutionUnknown": "La resolució d'aquest flux no s'ha pogut investigar. Heu d'establir manualment la resolució de detecció a Configuració o a la configuració."
}
}
},
@@ -1297,7 +1298,13 @@
"enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>",
"disableLabel": "Càmeres inhabilitades",
"disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.",
"enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis."
"enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis.",
"friendlyName": {
"edit": "Edita el nom de la pantalla de la càmera",
"title": "Edita el nom de la pantalla",
"description": "Estableix el nom amigable que es mostra per a aquesta càmera a tota la interfície d'usuari de la Fragata. Deixeu-ho en blanc per utilitzar l'ID de la càmera.",
"rename": "Canvia el nom"
}
},
"cameraConfig": {
"add": "Afegeix una càmera",
@@ -1347,6 +1354,14 @@
"inherit": "Hereta",
"enabled": "Habilitat",
"disabled": "Desactivat"
},
"cameraType": {
"title": "Tipus de càmera",
"label": "Tipus de càmera",
"description": "Estableix el tipus per a cada càmera. Les càmeres LPR dedicades són càmeres d'un sol ús amb un potent zoom òptic per capturar matrícules en vehicles distants. La majoria de les càmeres haurien d'utilitzar el tipus de càmera normal llevat que la càmera sigui específicament per a LPR i tingui una vista molt centrada en les matrícules.",
"dedicatedLpr": "LPR dedicat",
"saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia la fragata per aplicar els canvis.",
"normal": "Normal"
}
},
"cameraReview": {
@@ -1659,7 +1674,16 @@
"empty": "No hi ha etiquetes disponibles",
"allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions."
},
"addCustomLabel": "Afegeix una etiqueta personalitzada..."
"addCustomLabel": "Afegeix una etiqueta personalitzada...",
"genaiModel": {
"placeholder": "Selecciona el model…",
"search": "Cerca models…",
"noModels": "No hi ha models disponibles"
},
"knownPlates": {
"namePlaceholder": "per exemple. Cotxe de la parella",
"platePlaceholder": "Matricula o regex"
}
},
"globalConfig": {
"title": "Configuració global",
@@ -1704,7 +1728,22 @@
"overriddenGlobal": "Sobreescrit (Global)",
"overriddenGlobalTooltip": "Aquesta càmera anul·la la configuració global d'aquesta secció",
"overriddenBaseConfig": "Sobreescrit (Configuració base)",
"overriddenBaseConfigTooltip": "El perfil {{profile}} substitueix la configuració d'aquesta secció"
"overriddenBaseConfigTooltip": "El perfil {{profile}} substitueix la configuració d'aquesta secció",
"overriddenInCameras": {
"label_one": "Sobreescrit a la càmera {{count}}",
"label_many": "Sobreescrit en {{count}} càmeres",
"label_other": "Sobreescrit en {{count}} càmeres",
"tooltip_one": "{{count}} la càmera anul·la els valors d'aquesta secció. Feu clic per veure els detalls.",
"tooltip_many": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.",
"tooltip_other": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.",
"heading_one": "Aquesta secció global té camps que estan sobreescrits a la càmera {{count}}.",
"heading_many": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.",
"heading_other": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.",
"othersField_one": "{{count}} altre",
"othersField_many": "{{count}} altres",
"othersField_other": "{{count}} altres",
"profilePrefix": "Perfil {{profile}}: {{fields}}"
}
},
"profiles": {
"title": "Perfils",
@@ -1805,7 +1844,8 @@
"review": {
"recordDisabled": "L'enregistrament està desactivat, els elements de revisió no es generaran.",
"detectDisabled": "La detecció d'objectes està desactivada. Els elements de revisió requereixen objectes detectats per categoritzar alertes i deteccions.",
"allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions."
"allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions.",
"genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. La fragata tornarà a la vista prèvia de les imatges."
},
"audio": {
"noAudioRole": "Cap flux té definit el rol d'àudio. Heu d'habilitar el rol d'àudio per a la detecció d'àudio perquè funcioni."
@@ -1814,15 +1854,18 @@
"audioDetectionDisabled": "La detecció d'àudio no està activada per a aquesta càmera. La transcripció d'àudio requereix que la detecció d'àudio estigui activa."
},
"detect": {
"fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5."
"fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5. Els valors més alts poden causar problemes de rendiment i no proporcionaran cap benefici.",
"disabled": "La detecció d'objectes està desactivada. Les instantànies, articles de revisió i enriquiments com el reconeixement de rostres, el reconeixement de matrícules i la IA Generativa no funcionaran."
},
"faceRecognition": {
"globalDisabled": "El reconeixement de cares no està habilitat a nivell global. Habilita-ho en la configuració global per al reconeixement facial a nivell de càmera per funcionar.",
"personNotTracked": "El reconeixement de cares requereix que l'objecte 'persona' sigui rastrejat. Assegureu-vos que «persona» estigui a la llista de seguiment d'objectes."
"globalDisabled": "L'enriquiment del reconeixement facial s'ha d'habilitar perquè les funcions de reconeixement facial funcionin en aquesta càmera.",
"personNotTracked": "El reconeixement de cares requereix que l'objecte 'persona' sigui rastrejat. Habilita «persona» en objectes per a aquesta càmera.",
"modelSizeLarge": "El model 'gran' requereix una GPU o NPU per a un rendiment raonable. Usa «petit» en sistemes només de CPU."
},
"lpr": {
"globalDisabled": "El reconeixement de la matrícula no està habilitat a nivell global. Habilita-ho en la configuració global per al funcionament de LPR a nivell de càmera.",
"vehicleNotTracked": "El reconeixement de la matrícula requereix que es faci un seguiment del 'cotxe' o de la 'motocicleta'."
"globalDisabled": "L'enriquiment de reconeixement de matrícules ha d'estar habilitat perquè les funcions LPR funcionin en aquesta càmera.",
"vehicleNotTracked": "El reconeixement de la matrícula requereix que es faci un seguiment del 'cotxe' o de la 'motocicleta'.",
"modelSizeLarge": "El model 'gran' està optimitzat per a matrícules multilínies. El model 'petit' proporciona un millor rendiment sobre 'gran' i s'ha d'utilitzar tret que la vostra regió utilitzi formats de placa multilínia."
},
"record": {
"noRecordRole": "Cap flux té el rol de registre definit. L'enregistrament no funcionarà."
@@ -1836,6 +1879,12 @@
"detectors": {
"mixedTypes": "Tots els detectors han d'utilitzar el mateix tipus. Elimina els detectors existents per utilitzar un tipus diferent.",
"mixedTypesSuggestion": "Tots els detectors han d'utilitzar el mateix tipus. Suprimiu detectors existents o seleccioneu {{type}}."
},
"objects": {
"genaiNoDescriptionsProvider": "Heu de configurar un proveïdor de GenAI amb el rol 'descripcions' per a les descripcions que es generaran."
},
"semanticSearch": {
"jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta."
}
}
}
+3
View File
@@ -213,6 +213,9 @@
"expectedFps": "FPS esperat",
"reconnectsLastHour": "Reconnecta (última hora)",
"stallsLastHour": "Parades (última hora)"
},
"noCameras": {
"title": "No s'ha trobat cap càmera"
}
},
"lastRefreshed": "Darrera actualització: ",
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+2 -1
View File
@@ -250,7 +250,8 @@
"classification": "Klassifizierung",
"actions": "Aktion",
"chat": "Chat",
"profiles": "Profile"
"profiles": "Profile",
"features": "Funktionen"
},
"unit": {
"speed": {
+1 -1
View File
@@ -66,7 +66,7 @@
"label": "Kameras",
"desc": "Wähle Kameras für diese Gruppe aus."
},
"label": "Kameragruppen",
"label": "Kamera Gruppen",
"edit": "Kameragruppe bearbeiten",
"success": "Kameragruppe {{name}} wurde gespeichert."
},
+65 -4
View File
@@ -64,20 +64,73 @@
"toast": {
"error": {
"endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen",
"failed": "Fehler beim Starten des Exports: {{error}}",
"failed": "Fehler beim Export in die Warteschlange: {{error}}",
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
},
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.",
"view": "Ansicht"
"view": "Ansicht",
"queued": "Export in Warteschlange gestellt. Fortschritt auf der Exportseite verfolgen.",
"batchSuccess_one": "1 Export gestartet. Öffne den Fall jetzt.",
"batchSuccess_other": "{{count}} Exports gestartet. Öffne den Fall jetzt.",
"batchPartial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagene Kameras: {{failedCameras}}",
"batchFailed": "Fehler beim Starten der {{total}} Exporte. Fehlgeschlagene Kameras: {{failedCameras}}",
"batchQueuedSuccess_one": "1 Export in die Warteschlange gestellt. Fall wird jetzt geöffnet.",
"batchQueuedSuccess_other": "{{count}} Exporte in der Warteschlange. Fall wird jetzt geöffnet.",
"batchQueuedPartial": "{{successful}} von {{total}} Exporten in die Warteschlange gestellt. Fehlerhafte Kameras: {{failedCameras}}",
"batchQueueFailed": "Fehler beim Einreihen von {{total}} Exporten in die Warteschlange. Fehlerhafte Kameras: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Export speichern",
"previewExport": "Exportvorschau"
"previewExport": "Exportvorschau",
"queueingExport": "Export wird in die Warteschlange gestellt...",
"useThisRange": "Nutzen Sie diesen Bereich"
},
"export": "Exportieren",
"case": {
"label": "Fall",
"placeholder": "Einen Fall auswählen"
"placeholder": "Einen Fall auswählen",
"newCaseOption": "Einen neuen Fall erstellen",
"newCaseNamePlaceholder": "Neuer Fallname",
"newCaseDescriptionPlaceholder": "Fall Beschreibung",
"nonAdminHelp": "Für diese Exporte wird ein neuer Fall angelegt."
},
"queueing": "Export wird in die Warteschlange gestellt...",
"tabs": {
"export": "Einzelne Kamera",
"multiCamera": "Mehrere-Kameras"
},
"multiCamera": {
"timeRange": "Zeitbereich",
"selectFromTimeline": "Wählen Sie aus der Zeitleiste aus",
"cameraSelection": "Kameras",
"cameraSelectionHelp": "Kameras, die in diesem Zeitbereich Objekte verfolgen, sind vorausgewählt",
"checkingActivity": "Kameraaktivität wird überprüft...",
"noCameras": "keine kamaeras verfügbar",
"detectionCount_one": "1 verfolgtes Objekt",
"detectionCount_other": "{{count}} verfolgtesObjekte",
"nameLabel": "Export Name",
"namePlaceholder": "Optionaler Basisname für diese Exporte",
"queueingButton": "Exporte werden in die Warteschlange gestellt...",
"exportButton_one": "Export 1 Kamera",
"exportButton_other": "xport {{count}} Kameras"
},
"multi": {
"title_one": "1 Bewertung exportieren",
"title_other": "{{count}} Bewertung exportieren",
"description": "Exportieren Sie jede ausgewählte Rezension. Alle Exporte werden in einem einzigen Fall zusammengefasst.",
"descriptionNoCase": "Jede ausgewählte Bewertung exportieren.",
"caseNamePlaceholder": "Export prüfen - {{date}}",
"exportButton_one": "1 Bewertung exportieren",
"exportButton_other": "{{count}} Bewertung exportieren",
"exportingButton": "Exportieren...",
"toast": {
"started_one": "1 Export gestartet. Fall wird jetzt geöffnet.",
"started_other": "{{count}} Exporte gestartet. Fall wird jetzt geöffnet.",
"startedNoCase_one": "1 Export gestartet.",
"startedNoCase_other": "{{count}} Exports gestartet.",
"partial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagen: {{failedItems}}",
"failed": "Fehler beim Starten der {{total}} Exporte. Fehler: {{failedItems}}"
}
}
},
"streaming": {
@@ -125,6 +178,14 @@
"markAsReviewed": "Als geprüft markieren",
"deleteNow": "Jetzt löschen",
"markAsUnreviewed": "Als ungeprüft markieren"
},
"shareTimestamp": {
"label": "Zeitstempel teilen",
"title": "Zeitstempel teilen",
"description": "Teile eine URL mit Zeitstempel, die die aktuelle Position des Players angibt, oder wähle einen benutzerdefinierten Zeitstempel aus. Beachte, dass es sich hierbei nicht um eine öffentliche Freigabe-URL handelt und dass nur Benutzer Zugriff darauf haben, die Zugriff auf Frigate und diese Kamera haben.",
"custom": "Benutzerdefinierter Zeitstempel",
"button": "URL des Zeitstempels teilen",
"shareTitle": "Zeitstempel der Fregattenbewertung: {{camera}}"
}
},
"imagePicker": {
+2 -1
View File
@@ -3,7 +3,8 @@
"noPreviewFound": "Keine Vorschau gefunden",
"submitFrigatePlus": {
"title": "Dieses Bild an Frigate+ senden?",
"submit": "Senden"
"submit": "Absenden",
"previewError": "Schnappschuss Vorschau konnte nicht geladen werden. Die Aufnahme ist möglicherweise derzeit nicht verfügbar."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 oder höher ist für diesen Typ eines Live-Streams erforderlich.",
"streamOffline": {
+5 -1
View File
@@ -9,7 +9,7 @@
"description": "Aktiviert"
},
"audio": {
"label": "Audioereignisse",
"label": "Audioerkennung",
"description": "Einstellungen für audiobasierte Ereigniserkennung für diese Kamera.",
"enabled": {
"label": "Aktivieren der Audioerkennung",
@@ -537,6 +537,10 @@
"hwaccel_args": {
"label": "hwaccel-Argumente exportieren",
"description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen."
},
"max_concurrent": {
"label": "Maximale Anzahl gleichzeitiger Exporte",
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
}
},
"preview": {
+8 -4
View File
@@ -8,7 +8,7 @@
"description": "Wenn aktiviert, startet Frigate im abgesicherten Modus mit reduzierten Features für die Fehlersuche."
},
"audio": {
"label": "Audioereignisse",
"label": "Audioerkennung",
"enabled": {
"label": "Aktivieren der Audioerkennung",
"description": "Aktivieren oder deaktivieren Sie die Erkennung von Audioereignissen für alle Kameras; diese Einstellung kann für jede Kamera individuell überschrieben werden."
@@ -538,8 +538,8 @@
"description": "Aktivieren Sie die prozessbezogene Überwachung der Netzwerkbandbreite für Kamera-FFmpeg-Prozesse und Detektoren (erfordert entsprechende Funktionen)."
},
"intel_gpu_device": {
"label": "SR-IOV-Gerät",
"description": "Gerätekennung, die verwendet wird, wenn Intel-GPUs als SR-IOV behandelt werden, um die GPU-Statistiken zu korrigieren."
"label": "Intel GPU",
"description": "PCI-Bus-Adresse oder DRM-Gerätepfad (z. B. /dev/dri/card1), der verwendet wird, um die Intel-GPU-Statistiken einem bestimmten Gerät zuzuordnen, wenn mehrere vorhanden sind."
}
},
"version_check": {
@@ -1357,6 +1357,10 @@
"hwaccel_args": {
"label": "hwaccel-Argumente exportieren",
"description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen."
},
"max_concurrent": {
"label": "Maximale Anzahl gleichzeitiger Exporte",
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
}
},
"preview": {
@@ -1710,7 +1714,7 @@
},
"roles": {
"label": "Rollen",
"description": "GenAI-Rollen (Tools, Vision, Einbettungen); ein Anbieter pro Rolle."
"description": "GenAI-Rollen (Nachrichten, Beschreibung, Einbettungen); ein Anbieter pro Rolle."
},
"provider_options": {
"label": "Anbieter Optionen",
+46
View File
@@ -0,0 +1,46 @@
{
"documentTitle": "Chat - Frigate",
"title": "Frigate Chat",
"subtitle": "Ihr KI-Assistent für die Kameraverwaltung und Analysen",
"placeholder": "Frag mich alles...",
"error": "Es ist ein Fehler aufgetreten. Bitte versuche es erneut.",
"processing": "Wird verarbeitet...",
"toolsUsed": "Verwendet: {{tools}}",
"showTools": "Werkzeuge anzeigen ({{count}})",
"hideTools": "Werkzeuge ausblenden",
"call": "Anruf",
"result": "Ergebnis",
"arguments": "Argumente:",
"response": "Antwort:",
"attachment_chip_label": "{{label}} auf der {{camera}}",
"attachment_chip_remove": "Anhang entfernen",
"open_in_explore": "In „Explore“ öffnen",
"attach_event_aria": "Ereignis {{eventId}} hinzufügen",
"attachment_picker_paste_label": "Oder fügen Sie die Ereignis-ID ein",
"attachment_picker_attach": "Anhängen",
"attachment_picker_placeholder": "Ereignis hinzufügen",
"quick_reply_find_similar": "Ähnliche Sichtungen finden",
"quick_reply_tell_me_more": "Erzähl mir mehr darüber",
"quick_reply_when_else": "Wann wurde es sonst noch gesehen?",
"quick_reply_find_similar_text": "Ähnliche Sichtungen finden.",
"quick_reply_tell_me_more_text": "Erzähl mir mehr darüber.",
"quick_reply_when_else_text": "Wann gab es das sonst noch?",
"anchor": "Referenz",
"similarity_score": "Ähnlichkeit",
"no_similar_objects_found": "Es wurden keine ähnlichen Objekte gefunden.",
"semantic_search_required": "Die semantische Suche muss aktiviert sein, um ähnliche Objekte zu finden.",
"send": "Senden",
"suggested_requests": "Versuchen Sie doch mal zu fragen:",
"starting_requests": {
"show_recent_events": "Aktuelle Ereignisse anzeigen",
"show_camera_status": "Kamerastatus anzeigen",
"recap": "Was ist passiert, während ich weg war?",
"watch_camera": "Beobachten Sie eine Kamera auf Bewegungen"
},
"starting_requests_prompts": {
"show_recent_events": "Zeige mir die Ereignisse der letzten Stunde",
"show_camera_status": "Wie ist der aktuelle Status meiner Kameras?",
"recap": "Was ist passiert, während ich weg war?",
"watch_camera": "Pass auf die Haustür auf und sag mir Bescheid, wenn jemand kommt"
}
}
+3 -1
View File
@@ -25,7 +25,9 @@
},
"documentTitle": "Überprüfung - Frigate",
"recordings": {
"documentTitle": "Aufnahmen - Frigate"
"documentTitle": "Aufnahmen - Frigate",
"invalidSharedLink": "Der Link zur zeitgestempelten Aufzeichnung kann aufgrund eines Parsing-Fehlers nicht geöffnet werden.",
"invalidSharedCamera": "Der Link zur zeitgestempelten Aufzeichnung kann nicht geöffnet werden, da es sich um eine unbekannte oder nicht autorisierte Kamera handelt."
},
"calendarFilter": {
"last24Hours": "Letzte 24 Stunden"
+5 -2
View File
@@ -73,7 +73,7 @@
"label": "Schnappschuss Bewertung"
},
"score": {
"label": "Ergebnis"
"label": "Treffer"
},
"editAttributes": {
"title": "Attribute bearbeiten",
@@ -282,7 +282,10 @@
"zones": "Zonen",
"ratio": "Verhältnis",
"area": "Bereich",
"score": "Bewertung"
"score": "Bewertung",
"computedScore": "Berechnetes Ergebnis",
"topScore": "Bester Treffer",
"toggleAdvancedScores": "Erweiterte Ergebnisse umschalten"
}
},
"annotationSettings": {
+91 -2
View File
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}",
"assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}"
"assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}",
"caseSaveFailed": "Fehler beim speichern vom Fall: {{errorMessage}}",
"caseDeleteFailed": "Fehler beim löschem vom Fall: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Video herunterladen",
"editName": "Name ändern",
"deleteExport": "Export löschen",
"assignToCase": "Hinzufügen zum Fall"
"assignToCase": "Hinzufügen zum Fall",
"removeFromCase": "Vom Gehäuse entfernen"
},
"headings": {
"cases": "Fälle",
@@ -35,5 +38,91 @@
"newCaseOption": "Neuen Fall erstellen",
"nameLabel": "Fallname",
"descriptionLabel": "Beschreibung"
},
"toolbar": {
"newCase": "Neuer Fall",
"addExport": "Zum expotieren hinzufügen",
"editCase": "Fall bearbeiten",
"deleteCase": "Fall löschen"
},
"deleteCase": {
"label": "Fall löschen",
"desc": "Sind sie sich sicher löschen von{{caseName}}?",
"descKeepExports": "Exporte bleiben als nicht kategorisierte Exporte verfügbar.",
"descDeleteExports": "Alle Exporte werden in diesem Fall endgültig gelöscht.",
"deleteExports": "Exporte auch löschen"
},
"caseCard": {
"emptyCase": "Noch keine Exporte"
},
"jobCard": {
"defaultName": "{{camera}} export",
"queued": "In der Warteschlange",
"running": "läuft",
"preparing": "Vorbereitung",
"copying": "kopieren",
"encoding": "Codierung",
"encodingRetry": "Kodierung (Wiederholung)",
"finalizing": "Abschließen"
},
"caseView": {
"noDescription": "keine Beschreibung",
"createdAt": "Erstellt {{value}}",
"exportCount_one": "1 Export",
"exportCount_other": "{{count}} Exports",
"cameraCount_one": "1 Kamera",
"cameraCount_other": "{{count}} Kameras",
"showMore": "Mehr anzeigen",
"showLess": "Weniger Anzeigen",
"emptyTitle": "Der Fall ist leer",
"emptyDescription": "Fügen Sie vorhandene, nicht kategorisierte Exporte hinzu, um den Fall übersichtlich zu halten.",
"emptyDescriptionNoExports": "Es sind noch keine nicht kategorisierten Exporte zum Hinzufügen verfügbar."
},
"caseEditor": {
"createTitle": "Fall erstellen",
"editTitle": "Fall bearbeiten",
"namePlaceholder": "Fall Name",
"descriptionPlaceholder": "Fügen Sie Anmerkungen oder Kontext zu diesem Fall hinzu"
},
"addExportDialog": {
"title": "Export zum {{caseName}} hinzufügen",
"searchPlaceholder": "Suche nach nicht kategorisierten Exporten",
"empty": "Es wurden keine nicht kategorisierten Exporte gefunden, die dieser Suche entsprechen.",
"addButton_one": "1 Export hinzufügen",
"addButton_other": "Fügen Sie {{count}} Exporte hinzu",
"adding": "Hinzufügen..."
},
"selected_one": "{{count}} ausgewählt",
"selected_other": "{{count}} ausgewählt",
"bulkActions": {
"addToCase": "Zum Fall hinzufügen",
"moveToCase": "Zum Fall wechseln",
"removeFromCase": "Aus dem Fall nehmen",
"delete": "löschen",
"deleteNow": "jetzt löschen"
},
"bulkDelete": {
"title": "Exporte löschen",
"desc_one": "Möchten Sie den Export {{count}} wirklich löschen?",
"desc_other": "Möchten Sie wirklich {{count}} Exporte löschen?"
},
"bulkRemoveFromCase": {
"title": "Aus dem Fall nehmen",
"desc_one": "{{count}}-Export aus diesem Fall entfernen?",
"desc_other": "{{count}} Exporte aus diesem Fall entfernen?",
"descKeepExports": "Die Exporte werden in die Kategorie „Nicht kategorisiert“ verschoben.",
"descDeleteExports": "Exporte werden endgültig gelöscht.",
"deleteExports": "Löschen Sie stattdessen Exporte"
},
"bulkToast": {
"success": {
"delete": "Exporte erfolgreich gelöscht",
"reassign": "Fallzuweisung erfolgreich aktualisiert",
"remove": "Exporte erfolgreich aus dem Fall entfernt"
},
"error": {
"deleteFailed": "Fehler beim Löschen der Exporte: {{errorMessage}}",
"reassignFailed": "Fehler beim Aktualisieren der Fallzuordnung: {{errorMessage}}"
}
}
}
+2 -1
View File
@@ -62,7 +62,8 @@
},
"recording": {
"disable": "Aufzeichnung deaktivieren",
"enable": "Aufzeichnung aktivieren"
"enable": "Aufzeichnung aktivieren",
"disabledInConfig": "Aufnahme muss erst in den Einstellung für diese Kamera aktiviert werden."
},
"snapshots": {
"enable": "Schnappschüsse aktivieren",
@@ -0,0 +1,75 @@
{
"documentTitle": "Bewegungssuche - Frigate",
"title": "Bewegungssuche",
"description": "Zeichnen Sie ein Polygon, um den gewünschten Bereich zu definieren, und geben Sie einen Zeitbereich an, um innerhalb dieses Bereichs nach Bewegungsänderungen zu suchen.",
"selectCamera": "Die Bewegungssuche wird geladen",
"startSearch": "Suche starten",
"searchStarted": "Die Suche wurde gestartet",
"searchCancelled": "Suche abgebrochen",
"cancelSearch": "Abbrechen",
"searching": "Suche läuft.",
"searchComplete": "Suche abgeschlossen",
"noResultsYet": "Führen Sie eine Suche durch, um Bewegungsänderungen im ausgewählten Bereich zu finden",
"noChangesFound": "Im ausgewählten Bereich wurden keine Pixeländerungen festgestellt",
"changesFound_one": "Es wurde {{count}} Bewegungsänderungen gefunden",
"changesFound_other": "Es wurden {{count}} Bewegungsänderungen gefunden",
"framesProcessed": "{{count}} Bilder verarbeitet",
"jumpToTime": "Zu diesem Zeitpunkt springen",
"results": "Ergebnisse",
"showSegmentHeatmap": "Heatmap",
"newSearch": "Neue Suche",
"clearResults": "Eindeutige Ergebnisse",
"clearROI": "Polygon löschen",
"polygonControls": {
"points_one": "{{count}} Punkt",
"points_other": "{{count}} Punkte",
"undo": "Letzten Schritt rückgängig machen",
"reset": "Polygon zurücksetzen"
},
"motionHeatmapLabel": "Bewegungs-Heatmap",
"dialog": {
"title": "Bewegungssuche",
"cameraLabel": "Kamera",
"previewAlt": "Kamera-Vorschau für {{camera}}"
},
"timeRange": {
"title": "Suchbereich",
"start": "Startzeit",
"end": "Endzeit"
},
"settings": {
"title": "Sucheinstellungen",
"parallelMode": "Parallelbetrieb",
"parallelModeDesc": "Mehrere Aufzeichnungssegmente gleichzeitig scannen (schneller, aber deutlich rechenintensiver)",
"threshold": "Empfindlichkeitsschwelle",
"thresholdDesc": "Niedrigere Werte erkennen geringere Veränderungen (1255)",
"minArea": "Mindestwechselbereich",
"minAreaDesc": "Mindestanteil der untersuchten Region, der sich ändern muss, damit die Veränderung als signifikant gilt",
"frameSkip": "Bild überspringen",
"frameSkipDesc": "Verarbeite jeden N-ten Frame. Stelle diesen Wert auf die Bildrate deiner Kamera ein, um einen Frame pro Sekunde zu verarbeiten (z. B. 5 für eine Kamera mit 5 FPS, 30 für eine Kamera mit 30 FPS). Höhere Werte sorgen für eine schnellere Verarbeitung, können jedoch kurze Bewegungsabläufe übersehen.",
"maxResults": "Maximale Ergebnisse",
"maxResultsDesc": "Nach dieser Anzahl übereinstimmender Zeitstempel anhalten"
},
"errors": {
"noCamera": "Bitte wählen Sie eine Kamera aus",
"noROI": "Bitte zeichnen Sie einen Bereich von Interesse ein",
"noTimeRange": "Bitte wählen Sie einen Zeitraum aus",
"invalidTimeRange": "Die Endzeit muss nach der Startzeit liegen",
"searchFailed": "Suche fehlgeschlagen: {{message}}",
"polygonTooSmall": "Ein Polygon muss mindestens 3 Punkte haben",
"unknown": "Unbekannter Fehler"
},
"changePercentage": "Um {{percentage}} % verändert",
"metrics": {
"title": "Suchmetriken",
"segmentsScanned": "Durchsuchte Segmente",
"segmentsProcessed": "Bearbeitet",
"segmentsSkippedInactive": "Übersprungen (keine Aktivität)",
"segmentsSkippedHeatmap": "Übersprungen (keine Überschneidung der ROI)",
"fallbackFullRange": "Ausweich-Vollbereichsscan",
"framesDecoded": "Rahmen decodiert",
"wallTime": "Suchzeit",
"segmentErrors": "Segmentfehler",
"seconds": "{{seconds}}s"
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"title": "Debug-Wiedergabe",
"description": "Spielen Sie Kameraaufnahmen zur Fehlerbehebung ab. Die Objektliste zeigt eine zeitversetzte Übersicht der erkannten Objekte an, und auf der Registerkarte „Meldungen“ wird ein Stream der internen Meldungen von Frigate aus dem Wiedergabematerial angezeigt.",
"websocket_messages": "Nachrichten",
"dialog": {
"title": "Debug-Wiedergabe starten",
"description": "Erstellen Sie eine temporäre Wiedergabekamera, die historisches Bildmaterial in einer Schleife wiedergibt, um Probleme bei der Objekterkennung und -verfolgung zu beheben. Die Wiedergabekamera verfügt über dieselbe Erkennungskonfiguration wie die Quellkamera. Wählen Sie einen Zeitbereich aus, ab dem die Wiedergabe beginnen soll.",
"camera": "Quellkamera",
"timeRange": "Zeitraum",
"preset": {
"1m": "Letzte Minute",
"5m": "Die letzten 5 Minuten",
"timeline": "Aus der Zeitleiste",
"custom": "Benutzerdefiniert"
},
"startButton": "Wiedergabe starten",
"selectFromTimeline": "Auswählen",
"starting": "Wiedergabe wird gestartet...",
"startLabel": "Start",
"endLabel": "Ende",
"toast": {
"error": "Fehler beim Starten der Debug-Wiedergabe: {{error}}",
"alreadyActive": "Eine Wiederholungssitzung ist bereits aktiv",
"stopError": "Die Wiedergabe der Debug-Daten konnte nicht beendet werden: {{error}}",
"goToReplay": "Zur Aufzeichnung"
}
},
"page": {
"noSession": "Keine aktive Debug-Wiedergabesitzung",
"noSessionDesc": "Starten Sie eine Debug-Wiedergabe aus der Verlaufsansicht, indem Sie in der Symbolleiste auf die Schaltfläche „Aktionen“ klicken und „Debug-Wiedergabe“ auswählen.",
"goToRecordings": "Zur Historie",
"preparingClip": "Clip wird vorbereitet…",
"preparingClipDesc": "Frigate fasst die Aufzeichnungen für den ausgewählten Zeitraum zusammen. Bei längeren Zeiträumen kann dies eine Minute dauern.",
"startingCamera": "Debug-Wiedergabe wird gestartet…",
"startError": {
"title": "Debug Replay konnte nicht gestartet werden",
"back": "Zurück zur Übersicht"
},
"sourceCamera": "Quell Kamera",
"replayCamera": "Wiederholungskamera",
"initializingReplay": "Debug-Wiedergabe wird initialisiert...",
"stoppingReplay": "Debug-Wiedergabe wird angehalten...",
"stopReplay": "Stopp Wiederholung",
"confirmStop": {
"title": "Debug-Wiedergabe anhalten?",
"description": "Dadurch wird die Sitzung beendet und alle temporären Daten werden gelöscht. Sind Sie sicher?",
"confirm": "Anhalten Wiederholen",
"cancel": "Abbrechen"
},
"activity": "Aktivität",
"objects": "Objektliste",
"audioDetections": "Audioerkennungen",
"noActivity": "Es wurde keine Aktivität festgestellt",
"activeTracking": "Aktive Verfolgung",
"noActiveTracking": "Keine aktive Nachverfolgung",
"configuration": "Konfiguration",
"configurationDesc": "Passen Sie die Einstellungen für die Bewegungserkennung und die Objektverfolgung der Debug-Replay-Kamera an. Es werden keine Änderungen in Ihrer Frigate-Konfigurationsdatei gespeichert."
}
}

Some files were not shown because too many files have changed in this diff Show More