import { GxState } from "./state.js" import { SettingsPage } from "./settings.js" import { GxNotice } from "./notice.js" import { BluetoothNotice, bluetoothConnection } from "./bluetooth-notice.js" import { SentryNotifications } from "./sentry-notifications.js" import { SnapshotFeed } from "./snapshot-feed.js" import { deleteRecords, storedSize } from "./record-deletion.js" // Historical motion records and independently configured notifications. const ID = /^[0-9a-f]{32}$/ export function validSentryEvents(value) { if (value?.schemaVersion !== 1 || value.source !== "local" || typeof value.scanIncomplete !== "boolean" || value.capacity !== 512 || !Array.isArray(value.events) || value.events.length > 512) return false if (value.storageBytes !== undefined && (!Number.isSafeInteger(value.storageBytes) || value.storageBytes < 0)) return false if (value.storageIncomplete !== undefined && typeof value.storageIncomplete !== "boolean") return false const seen = new Set() return value.events.every((event) => { if (event === null || typeof event !== "object" || Object.keys(event).some((key) => !["eventId", "kind", "systemTimeMs", "images", "simulated"].includes(key)) || (event.simulated !== undefined && event.simulated !== true) || (event.images !== undefined && (!Array.isArray(event.images) || event.images.length > 2 || new Set(event.images).size !== event.images.length || event.images.some((camera) => !["wide", "cabin"].includes(camera)))) || typeof event.eventId !== "string" || !ID.test(event.eventId) || seen.has(event.eventId) || !["warning", "alarm", "selfie"].includes(event.kind) || !Number.isSafeInteger(event.systemTimeMs) || event.systemTimeMs <= 0 || event.systemTimeMs > 8_640_000_000_000_000) return false seen.add(event.eventId) return true }) } export const systemTime = (milliseconds) => Number.isSafeInteger(milliseconds) && milliseconds > 0 && milliseconds <= 8_640_000_000_000_000 ? new Date(milliseconds).toLocaleString() : "Unavailable" export class SentryEventsFeed extends SnapshotFeed { static endpoint = "./api/sentry/events" static valid = validSentryEvents static subject = "local motion events" static unavailable = "Local motion events are unavailable." static invalid = "Local motion records are unavailable." stop() { this.deleting = false; super.stop() } emit() { this.publish({ status: this.status, data: this.data, error: this.error, deleting: this.deleting === true }) } remove(eventId, confirm) { if (eventId != null && !ID.test(eventId)) return Promise.resolve(false) return deleteRecords(this, "./api/sentry/events/delete", eventId == null ? { all: true, confirmed: true } : { eventId }, { valid: validSentryEvents, confirm, subject: "Sentry events", accept: data => { this.data = data; this.status = "ready"; this.error = ""; this.emit() }, }) } } export const SentryEventsPage = { name: "SentryEventsPage", components: { GxState, GxNotice, BluetoothNotice, SentryNotifications, SettingsPage }, props: { mode: { type: String, required: true }, unauthorized: { type: Function, required: true }, go: { type: Function, required: true } }, data: () => ({ bluetooth: bluetoothConnection(), status: "idle", data: null, error: "", deleting: false }), created() { this.feed = new SentryEventsFeed({ publish: (update) => Object.assign(this.$data, update), unauthorized: this.unauthorized }) }, mounted() { this.visibility = () => { if (document.hidden) this.feed.stop(); else if (this.mode === "local") this.feed.start() } document.addEventListener("visibilitychange", this.visibility) if (this.mode === "local" && !document.hidden) this.feed.start() }, beforeUnmount() { document.removeEventListener("visibilitychange", this.visibility); this.feed.stop() }, methods: { systemTime, storedSize }, template: `

Sentry

Review motion events, configure motion monitoring, and manage notifications. Timestamps use the device clock and may be inaccurate.

Local motion records are unavailable in preview.
`, }