diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index f01314895..835a49b0c 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -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() {

Safe mode is active while UI rendering is being repaired.

Galaxy Pairing | + Sentry Mode | Toggles | Software

@@ -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), diff --git a/starpilot/system/the_galaxy/assets/components/sidebar.js b/starpilot/system/the_galaxy/assets/components/sidebar.js index 12e6f948f..f2228d2b4 100644 --- a/starpilot/system/the_galaxy/assets/components/sidebar.js +++ b/starpilot/system/the_galaxy/assets/components/sidebar.js @@ -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" }, diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json index 561ee7fc0..60385ae77 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json @@ -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", diff --git a/starpilot/system/the_galaxy/assets/components/tools/galaxy.css b/starpilot/system/the_galaxy/assets/components/tools/galaxy.css index a468f5ba5..2363d7d2c 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/galaxy.css +++ b/starpilot/system/the_galaxy/assets/components/tools/galaxy.css @@ -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 { diff --git a/starpilot/system/the_galaxy/assets/components/tools/galaxy.js b/starpilot/system/the_galaxy/assets/components/tools/galaxy.js index dc2b4ecfa..e6c53c84b 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/galaxy.js +++ b/starpilot/system/the_galaxy/assets/components/tools/galaxy.js @@ -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` -
-
-
-

Sentry Mode

-

View the latest movement event and captured images directly in Galaxy.

-
- ${() => state.sentryStatus.state || "unknown"} -
- - ${() => { - if (state.sentryLoading) return html`
Checking Sentry status…
` - const event = state.sentryEvent || {} - if (!event.eventId) return html`

No Sentry events recorded yet.

` - - return html` -
- ${String(event.kind || "event").toUpperCase()} - ${event.detectedAt || ""} -
-

${event.message || "Movement detected while parked."}

- ${Array.isArray(event.imageUrls) && event.imageUrls.length > 0 ? html` -
- ${event.imageUrls.map((url, index) => html` - - Sentry capture ${index + 1} - - `)} -
- ` : html`

No camera images were available for this event.

`} - ` - }} - - ${showTestButton ? html` - - ` : ""} -
- ` -} - 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` -
-
-
🛰️
-

Galaxy Pairing Unavailable via Galaxy

-

Galaxy pairing requires a direct connection.
Connect to your device's local network to use this feature.

-
- ${SentryEventPanel(false)} +
+
🛰️
+

Galaxy Pairing Unavailable via Galaxy

+

Galaxy pairing requires a direct connection.
Connect to your device's local network to use this feature.

`; } @@ -211,15 +114,6 @@ export function GalaxyPairing() { ${state.url} -
` } diff --git a/starpilot/system/the_galaxy/assets/components/tools/sentry.css b/starpilot/system/the_galaxy/assets/components/tools/sentry.css new file mode 100644 index 000000000..5109c65ea --- /dev/null +++ b/starpilot/system/the_galaxy/assets/components/tools/sentry.css @@ -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%; + } +} diff --git a/starpilot/system/the_galaxy/assets/components/tools/sentry.js b/starpilot/system/the_galaxy/assets/components/tools/sentry.js new file mode 100644 index 000000000..86dd6fb17 --- /dev/null +++ b/starpilot/system/the_galaxy/assets/components/tools/sentry.js @@ -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`

No Sentry events recorded yet.

` + + return html` +
+ ${String(event.kind || "event").toUpperCase()} + ${event.detectedAt || ""} +
+

${event.message || "Movement detected while parked."}

+ ${Array.isArray(event.imageUrls) && event.imageUrls.length > 0 ? html` +
+ ${event.imageUrls.map((url, index) => html` + + Sentry capture ${index + 1} + + `)} +
+ ` : html`

No camera images were available for this event.

`} + ` +} + +export function SentryMode() { + startPolling() + const remote = isGalaxyTunnel() + + return html` +
+
+
+

Sentry Mode

+

Monitor the parked vehicle and review movement captures directly in Galaxy.

+
+ ${() => state.status.state || "unknown"} +
+ +
+

Configuration

+

Galaxy is the built-in notification and image viewer. Webhook and ntfy delivery are optional.

+ + ${() => state.loading ? html`
Loading Sentry settings…
` : html` + + + + + + + + `} +
+ +
+
+
+

Latest Event

+

Events refresh automatically every five seconds.

+
+ ${remote ? "" : html` + + `} +
+ ${() => renderEvent()} +
+
+ ` +} diff --git a/starpilot/system/the_galaxy/templates/index.html b/starpilot/system/the_galaxy/templates/index.html index fbb18915b..06b20f8e5 100644 --- a/starpilot/system/the_galaxy/templates/index.html +++ b/starpilot/system/the_galaxy/templates/index.html @@ -41,6 +41,7 @@ + diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 0d18fb5ca..680879a1f 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -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",