mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-20 15:54:13 +08:00
Sentry Side Bar
This commit is contained in:
@@ -14,6 +14,7 @@ import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js
|
||||
import { SettingsView } from "/assets/components/settings.js"
|
||||
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
|
||||
import { Sidebar } from "/assets/components/sidebar.js?v=lateral-tuning-1"
|
||||
import { SentryMode } from "/assets/components/tools/sentry.js"
|
||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
|
||||
import { LivePlots } from "/assets/components/tools/plots.js"
|
||||
@@ -55,6 +56,7 @@ function SafeHome() {
|
||||
<p>Safe mode is active while UI rendering is being repaired.</p>
|
||||
<p>
|
||||
<a href="/galaxy">Galaxy Pairing</a> |
|
||||
<a href="/sentry">Sentry Mode</a> |
|
||||
<a href="/device_settings">Toggles</a> |
|
||||
<a href="/manage_updates">Software</a>
|
||||
</p>
|
||||
@@ -72,6 +74,7 @@ function Root() {
|
||||
createRoute("root", "/", Home),
|
||||
createRoute("routes", "/dashcam_routes", RouteRecordings),
|
||||
createRoute("screen_recordings", "/screen_recordings", ScreenRecordings),
|
||||
createRoute("sentry", "/sentry", SentryMode),
|
||||
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
|
||||
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
|
||||
createRoute("model_manager", "/manage_models", ModelManager),
|
||||
|
||||
@@ -14,6 +14,7 @@ const MENU_ITEMS = {
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
|
||||
{ name: "Sentry Mode", link: "/sentry", icon: "bi-shield-exclamation" },
|
||||
{ name: "Lateral Tuning", link: "/tuning", icon: "bi-sign-turn-right" },
|
||||
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
|
||||
{ name: "Maps", link: "/manage_maps", icon: "bi-map" },
|
||||
|
||||
@@ -13,40 +13,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sentry Mode",
|
||||
"icon": "bi-shield-exclamation",
|
||||
"params": [
|
||||
{
|
||||
"key": "SentryModeEnabled",
|
||||
"label": "Sentry Mode",
|
||||
"description": "Detect movement while parked and send alerts through Galaxy.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"settings_tier": "simple"
|
||||
},
|
||||
{
|
||||
"key": "SentryModeWebhook",
|
||||
"label": "Sentry Webhook",
|
||||
"description": "Optional webhook that Galaxy will POST to when sentry mode detects movement. Discord webhooks are supported.",
|
||||
"data_type": "string",
|
||||
"ui_type": "text",
|
||||
"input_type": "url",
|
||||
"placeholder": "https://…",
|
||||
"settings_tier": "simple"
|
||||
},
|
||||
{
|
||||
"key": "SentryModeNtfyUrl",
|
||||
"label": "Sentry ntfy URL",
|
||||
"description": "Optional ntfy topic URL, for example https://ntfy.sh/my-starpilot-topic.",
|
||||
"data_type": "string",
|
||||
"ui_type": "text",
|
||||
"input_type": "url",
|
||||
"placeholder": "https://ntfy.sh/…",
|
||||
"settings_tier": "simple"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lateral (Steering)",
|
||||
"icon": "bi-arrows-move",
|
||||
|
||||
@@ -129,80 +129,6 @@
|
||||
background-color: rgba(224, 85, 119, 0.35);
|
||||
}
|
||||
|
||||
/* ――― Sentry event viewer ――― */
|
||||
.sentry-event-widget {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
margin-top: var(--margin-lg);
|
||||
max-width: var(--width-xxxl);
|
||||
padding: var(--padding-lg);
|
||||
}
|
||||
|
||||
.sentry-event-heading {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: var(--gap-base);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sentry-event-heading h3 {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sentry-event-state,
|
||||
.sentry-event-kind {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: 999px;
|
||||
color: var(--main-fg);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 0.25rem 0.55rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sentry-event-meta {
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-size-xs);
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.sentry-event-message {
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sentry-image-grid {
|
||||
display: grid;
|
||||
gap: var(--gap-sm);
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.sentry-image-grid a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sentry-image-grid img {
|
||||
border: var(--border-style-main);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: block;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sentry-empty {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ――― Mobile ――― */
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.galaxy-wrapper {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { isGalaxyTunnel } from "/assets/js/utils.js"
|
||||
import { Modal } from "/assets/components/modal.js"
|
||||
import { requestSentryNotificationPermission } from "/assets/components/sentry_notifications.js"
|
||||
|
||||
const state = reactive({
|
||||
paired: false,
|
||||
@@ -11,14 +10,8 @@ const state = reactive({
|
||||
submitting: false,
|
||||
showUnpairModal: false,
|
||||
fetched: false,
|
||||
sentryLoading: true,
|
||||
sentryStatus: {},
|
||||
sentryEvent: {},
|
||||
sentryTestBusy: false,
|
||||
})
|
||||
|
||||
let sentryPollTimer = null
|
||||
|
||||
async function fetchStatus() {
|
||||
state.loading = true
|
||||
try {
|
||||
@@ -32,91 +25,6 @@ async function fetchStatus() {
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
async function fetchSentryStatus() {
|
||||
try {
|
||||
const res = await fetch("/api/sentry/status", { cache: "no-store" })
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
state.sentryStatus = data.status || {}
|
||||
state.sentryEvent = data.lastEvent || {}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch Sentry status:", e)
|
||||
} finally {
|
||||
state.sentryLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSentryPolling() {
|
||||
if (sentryPollTimer !== null) return
|
||||
fetchSentryStatus()
|
||||
sentryPollTimer = window.setInterval(fetchSentryStatus, 5000)
|
||||
}
|
||||
|
||||
async function sendSentryTest() {
|
||||
if (state.sentryTestBusy) return
|
||||
state.sentryTestBusy = true
|
||||
try {
|
||||
const res = await fetch("/api/sentry/test", { method: "POST" })
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
showSnackbar(data.error || "Sentry test failed.")
|
||||
return
|
||||
}
|
||||
showSnackbar("Sentry test started. Galaxy will show the captured images when ready.")
|
||||
} catch (e) {
|
||||
showSnackbar("Network error — is the device reachable?")
|
||||
} finally {
|
||||
state.sentryTestBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
function SentryEventPanel(showTestButton = true) {
|
||||
return html`
|
||||
<section class="sentry-event-widget">
|
||||
<div class="sentry-event-heading">
|
||||
<div>
|
||||
<h3>Sentry Mode</h3>
|
||||
<p class="galaxy-text">View the latest movement event and captured images directly in Galaxy.</p>
|
||||
</div>
|
||||
<span class="sentry-event-state">${() => state.sentryStatus.state || "unknown"}</span>
|
||||
</div>
|
||||
|
||||
${() => {
|
||||
if (state.sentryLoading) return html`<div class="galaxy-loading">Checking Sentry status…</div>`
|
||||
const event = state.sentryEvent || {}
|
||||
if (!event.eventId) return html`<p class="sentry-empty">No Sentry events recorded yet.</p>`
|
||||
|
||||
return html`
|
||||
<div class="sentry-event-meta">
|
||||
<span class="sentry-event-kind">${String(event.kind || "event").toUpperCase()}</span>
|
||||
<span>${event.detectedAt || ""}</span>
|
||||
</div>
|
||||
<p class="sentry-event-message">${event.message || "Movement detected while parked."}</p>
|
||||
${Array.isArray(event.imageUrls) && event.imageUrls.length > 0 ? html`
|
||||
<div class="sentry-image-grid">
|
||||
${event.imageUrls.map((url, index) => html`
|
||||
<a href="${url}" target="_blank" rel="noopener">
|
||||
<img src="${url}" alt="Sentry capture ${index + 1}" loading="lazy" />
|
||||
</a>
|
||||
`)}
|
||||
</div>
|
||||
` : html`<p class="sentry-empty">No camera images were available for this event.</p>`}
|
||||
`
|
||||
}}
|
||||
|
||||
${showTestButton ? html`
|
||||
<button
|
||||
class="galaxy-button"
|
||||
@click="${sendSentryTest}"
|
||||
disabled="${() => state.sentryTestBusy}"
|
||||
>
|
||||
${() => state.sentryTestBusy ? "Capturing test images…" : "Send test event"}
|
||||
</button>
|
||||
` : ""}
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
async function pair() {
|
||||
if (state.submitting) return
|
||||
const pw = state.password.trim()
|
||||
@@ -170,17 +78,12 @@ async function unpair() {
|
||||
}
|
||||
|
||||
export function GalaxyPairing() {
|
||||
ensureSentryPolling()
|
||||
|
||||
if (isGalaxyTunnel()) {
|
||||
return html`
|
||||
<div class="galaxy-wrapper">
|
||||
<div class="tunnel-notice">
|
||||
<div class="tunnel-notice-icon">🛰️</div>
|
||||
<h3 class="tunnel-notice-title">Galaxy Pairing Unavailable via Galaxy</h3>
|
||||
<p class="tunnel-notice-body">Galaxy pairing requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
|
||||
</div>
|
||||
${SentryEventPanel(false)}
|
||||
<div class="tunnel-notice">
|
||||
<div class="tunnel-notice-icon">🛰️</div>
|
||||
<h3 class="tunnel-notice-title">Galaxy Pairing Unavailable via Galaxy</h3>
|
||||
<p class="tunnel-notice-body">Galaxy pairing requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -211,15 +114,6 @@ export function GalaxyPairing() {
|
||||
<a class="galaxy-url" href="${state.url}" target="_blank" rel="noopener">
|
||||
${state.url}
|
||||
</a>
|
||||
<button
|
||||
class="galaxy-button"
|
||||
@click="${async () => {
|
||||
const permission = await requestSentryNotificationPermission()
|
||||
showSnackbar(permission === "granted" ? "Browser sentry notifications enabled." : "Browser notifications were not enabled.")
|
||||
}}"
|
||||
>
|
||||
Enable browser alerts
|
||||
</button>
|
||||
<button
|
||||
class="galaxy-button galaxy-button-danger"
|
||||
@click="${() => { state.showUnpairModal = true }}"
|
||||
@@ -267,7 +161,6 @@ export function GalaxyPairing() {
|
||||
</section>
|
||||
`
|
||||
}}
|
||||
${SentryEventPanel()}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
.sentry-page {
|
||||
max-width: var(--width-xxxl);
|
||||
padding: var(--padding-base) var(--padding-lg) var(--padding-xxl);
|
||||
}
|
||||
|
||||
.sentry-page-header,
|
||||
.sentry-card-heading,
|
||||
.sentry-setting-row,
|
||||
.sentry-field {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sentry-page-header {
|
||||
align-items: center;
|
||||
margin-bottom: var(--margin-lg);
|
||||
}
|
||||
|
||||
.sentry-page-header h2,
|
||||
.sentry-card h3 {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sentry-page-header p,
|
||||
.sentry-muted,
|
||||
.sentry-setting-row small,
|
||||
.sentry-field small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sentry-page-header p,
|
||||
.sentry-muted {
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.sentry-status-pill,
|
||||
.sentry-event-kind {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: 999px;
|
||||
color: var(--main-fg);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 0.25rem 0.55rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sentry-card {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: var(--margin-lg);
|
||||
padding: var(--padding-lg);
|
||||
}
|
||||
|
||||
.sentry-setting-row,
|
||||
.sentry-field {
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--sidebar-border-color);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.sentry-setting-row span,
|
||||
.sentry-field span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.sentry-input {
|
||||
background: var(--sidebar-bg);
|
||||
border: var(--border-style-main);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
min-width: min(100%, 28rem);
|
||||
padding: 0.55rem 0.7rem;
|
||||
}
|
||||
|
||||
.sentry-input:focus {
|
||||
border-color: var(--main-fg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sentry-toggle {
|
||||
accent-color: var(--main-fg);
|
||||
cursor: pointer;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.sentry-button {
|
||||
background: var(--input-bg);
|
||||
border: var(--border-style-main);
|
||||
border-radius: var(--border-radius-base);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
padding: 0.6rem 0.8rem;
|
||||
}
|
||||
|
||||
.sentry-button:hover:not(:disabled) {
|
||||
background: var(--main-fg);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.sentry-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sentry-button-secondary {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sentry-loading,
|
||||
.sentry-empty {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sentry-event-meta {
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-size-xs);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sentry-event-message {
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sentry-image-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.sentry-image-grid a,
|
||||
.sentry-image-grid img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sentry-image-grid img {
|
||||
border: var(--border-style-main);
|
||||
border-radius: var(--border-radius-base);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.sentry-page {
|
||||
padding: var(--padding-sm) var(--padding-base) var(--padding-xl);
|
||||
}
|
||||
|
||||
.sentry-page-header,
|
||||
.sentry-card-heading,
|
||||
.sentry-setting-row,
|
||||
.sentry-field {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sentry-input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { isGalaxyTunnel } from "/assets/js/utils.js"
|
||||
import { requestSentryNotificationPermission } from "/assets/components/sentry_notifications.js"
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
savingKey: "",
|
||||
params: {},
|
||||
status: {},
|
||||
event: {},
|
||||
testBusy: false,
|
||||
})
|
||||
|
||||
let pollTimer = null
|
||||
|
||||
async function fetchParams() {
|
||||
try {
|
||||
const response = await fetch("/api/params/all", { cache: "no-store" })
|
||||
if (response.ok) state.params = await response.json()
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Sentry settings:", error)
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch("/api/sentry/status", { cache: "no-store" })
|
||||
if (!response.ok) return
|
||||
const payload = await response.json()
|
||||
state.status = payload.status || {}
|
||||
state.event = payload.lastEvent || {}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Sentry status:", error)
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer !== null) return
|
||||
fetchParams()
|
||||
fetchStatus()
|
||||
pollTimer = window.setInterval(fetchStatus, 5000)
|
||||
}
|
||||
|
||||
async function saveParam(key, value) {
|
||||
state.savingKey = key
|
||||
try {
|
||||
const response = await fetch("/api/params", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, value }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) {
|
||||
showSnackbar(payload.error || `Failed to update ${key}.`)
|
||||
return
|
||||
}
|
||||
state.params = { ...state.params, ...(payload.updated || {}), [key]: value }
|
||||
showSnackbar(payload.message || "Sentry setting saved.")
|
||||
} catch (error) {
|
||||
showSnackbar("Network error — is the device reachable?")
|
||||
} finally {
|
||||
state.savingKey = ""
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTestEvent() {
|
||||
if (state.testBusy) return
|
||||
state.testBusy = true
|
||||
try {
|
||||
const response = await fetch("/api/sentry/test", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) {
|
||||
showSnackbar(payload.error || "Sentry test failed.")
|
||||
return
|
||||
}
|
||||
showSnackbar("Test capture started. The images will appear here shortly.")
|
||||
} catch (error) {
|
||||
showSnackbar("Network error — is the device reachable?")
|
||||
} finally {
|
||||
state.testBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvent() {
|
||||
const event = state.event || {}
|
||||
if (!event.eventId) return html`<p class="sentry-empty">No Sentry events recorded yet.</p>`
|
||||
|
||||
return html`
|
||||
<div class="sentry-event-meta">
|
||||
<span class="sentry-event-kind">${String(event.kind || "event").toUpperCase()}</span>
|
||||
<span>${event.detectedAt || ""}</span>
|
||||
</div>
|
||||
<p class="sentry-event-message">${event.message || "Movement detected while parked."}</p>
|
||||
${Array.isArray(event.imageUrls) && event.imageUrls.length > 0 ? html`
|
||||
<div class="sentry-image-grid">
|
||||
${event.imageUrls.map((url, index) => html`
|
||||
<a href="${url}" target="_blank" rel="noopener">
|
||||
<img src="${url}" alt="Sentry capture ${index + 1}" loading="lazy" />
|
||||
</a>
|
||||
`)}
|
||||
</div>
|
||||
` : html`<p class="sentry-empty">No camera images were available for this event.</p>`}
|
||||
`
|
||||
}
|
||||
|
||||
export function SentryMode() {
|
||||
startPolling()
|
||||
const remote = isGalaxyTunnel()
|
||||
|
||||
return html`
|
||||
<div class="sentry-page">
|
||||
<div class="sentry-page-header">
|
||||
<div>
|
||||
<h2>Sentry Mode</h2>
|
||||
<p>Monitor the parked vehicle and review movement captures directly in Galaxy.</p>
|
||||
</div>
|
||||
<span class="sentry-status-pill">${() => state.status.state || "unknown"}</span>
|
||||
</div>
|
||||
|
||||
<section class="sentry-card">
|
||||
<h3>Configuration</h3>
|
||||
<p class="sentry-muted">Galaxy is the built-in notification and image viewer. Webhook and ntfy delivery are optional.</p>
|
||||
|
||||
${() => state.loading ? html`<div class="sentry-loading">Loading Sentry settings…</div>` : html`
|
||||
<label class="sentry-setting-row">
|
||||
<span>
|
||||
<strong>Enable Sentry Mode</strong>
|
||||
<small>Detect sustained movement while the vehicle is parked.</small>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sentry-toggle"
|
||||
checked="${() => !!state.params.SentryModeEnabled}"
|
||||
disabled="${() => state.savingKey === "SentryModeEnabled"}"
|
||||
@change="${(event) => saveParam("SentryModeEnabled", !!event.currentTarget.checked)}" />
|
||||
</label>
|
||||
|
||||
<label class="sentry-field">
|
||||
<span><strong>Webhook URL</strong><small>Optional Discord-compatible or custom webhook.</small></span>
|
||||
<input
|
||||
class="sentry-input"
|
||||
type="url"
|
||||
value="${() => state.params.SentryModeWebhook || ""}"
|
||||
placeholder="https://…"
|
||||
disabled="${() => state.savingKey === "SentryModeWebhook"}"
|
||||
@change="${(event) => saveParam("SentryModeWebhook", event.currentTarget.value.trim())}" />
|
||||
</label>
|
||||
|
||||
<label class="sentry-field">
|
||||
<span><strong>ntfy URL</strong><small>Optional ntfy topic URL for phone notifications.</small></span>
|
||||
<input
|
||||
class="sentry-input"
|
||||
type="url"
|
||||
value="${() => state.params.SentryModeNtfyUrl || ""}"
|
||||
placeholder="https://ntfy.sh/…"
|
||||
disabled="${() => state.savingKey === "SentryModeNtfyUrl"}"
|
||||
@change="${(event) => saveParam("SentryModeNtfyUrl", event.currentTarget.value.trim())}" />
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="sentry-button"
|
||||
@click="${async () => {
|
||||
const permission = await requestSentryNotificationPermission()
|
||||
showSnackbar(permission === "granted" ? "Browser alerts enabled." : "Browser alerts were not enabled.")
|
||||
}}">
|
||||
Enable browser alerts
|
||||
</button>
|
||||
`}
|
||||
</section>
|
||||
|
||||
<section class="sentry-card">
|
||||
<div class="sentry-card-heading">
|
||||
<div>
|
||||
<h3>Latest Event</h3>
|
||||
<p class="sentry-muted">Events refresh automatically every five seconds.</p>
|
||||
</div>
|
||||
${remote ? "" : html`
|
||||
<button class="sentry-button sentry-button-secondary" @click="${sendTestEvent}" disabled="${() => state.testBusy}">
|
||||
${() => state.testBusy ? "Capturing…" : "Send test capture"}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
${() => renderEvent()}
|
||||
</section>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=favorite-actions-1">
|
||||
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/sentry.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/v_asm.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/pip_sidecam.css">
|
||||
|
||||
@@ -4182,6 +4182,8 @@ def setup(app):
|
||||
"/assets/components/tools/device_settings_layout.json",
|
||||
"/assets/components/tools/galaxy.js",
|
||||
"/assets/components/tools/galaxy.css",
|
||||
"/assets/components/tools/sentry.js",
|
||||
"/assets/components/tools/sentry.css",
|
||||
"/assets/components/tools/v_asm.js",
|
||||
"/assets/components/tools/v_asm.css",
|
||||
"/assets/components/tools/pip_sidecam.js",
|
||||
|
||||
Reference in New Issue
Block a user