Sentry Side Bar

This commit is contained in:
firestar5683
2026-08-15 11:32:04 -05:00
parent 12e9e1982c
commit d97a4fdd65
9 changed files with 376 additions and 219 deletions
@@ -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",