+
+
Settings
-
-
- Vessel Cloud
-
- Sign in to connect with Vessel Cloud tunnel servers
-
-
-
- {session ? (
-
-
-
- Signed in as
-
- {session.user?.email}
-
-
- Sign Out
-
-
- ) : (
-
- Sign in with Google
-
- )}
-
-
-
-
-
- Capsule Server
-
- Configure the Capsule AI server URL for chat and image analysis
-
-
-
-
-
Capsule Server URL
-
- setCapsuleUrl(e.target.value)}
- />
-
- {capsuleSaved ? "Saved" : "Save"}
-
-
-
-
- Default:{" "}
- {import.meta.env.VITE_CAPSULE_URL || "http://localhost:3000"}
-
-
-
-
- {session && (
-
-
- TURN Server
-
- Issue Cloudflare TURN credentials for WebRTC relay
-
-
-
-
- {isExpired ? (
-
- {turnLoading
- ? "Re-issuing..."
- : "Re-issue TURN Credentials"}
-
- ) : (
-
- {turnLoading ? "Issuing..." : "Issue TURN Credentials"}
-
- )}
- {isIssued && (
-
- Issued — expires{" "}
- {new Date(
- turnResult?.expiresAt ?? existingCred!.expiresAt,
- ).toLocaleTimeString()}
-
- )}
- {isExpired && (
-
- Expired
-
- )}
- {turnError && (
-
-
{turnError}
- {turnErrorUsage && (
-
- {formatTurnQuotaUsage(turnErrorUsage)}
-
- )}
-
- )}
-
- {isIssued && (
-
- {(turnResult?.iceServers ?? existingCred?.iceServers)
- ?.length ?? 0}{" "}
- ICE server(s) cached. Credentials will be applied
- automatically.
-
- )}
-
-
- )}
-
-
-
- Code workspace
-
-
-
-
-
- Code service
-
-
void handleCodeServiceToggle(v)}
- />
-
-
-
-
-
-
- Tunnel
-
-
-
-
-
- Tunnel Connection
-
-
-
-
-
-
-
-
- Session
- {status.session_id ? (
-
- {status.session_id}
-
- ) : (
- None
- )}
-
- {status.session_id && (
-
-
- Copy tunnel URL
-
-
- )}
-
-
-
-
- Refresh status
-
- {(error || localError) && (
-
- {localError ?? error}
+
+ {categories.map((c) => (
+
navigate(c.to)}
+ className='flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/50 transition-colors'
+ >
+ {c.icon}
+
+ {c.title}
+
+ {c.description}
- )}
-
-
-
+
+
+
+ ))}
+
diff --git a/apps/client/src/pages/integration/index.tsx b/apps/client/src/pages/settings/integration.tsx
similarity index 75%
rename from apps/client/src/pages/integration/index.tsx
rename to apps/client/src/pages/settings/integration.tsx
index 1ede5bd..329eb21 100644
--- a/apps/client/src/pages/integration/index.tsx
+++ b/apps/client/src/pages/settings/integration.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { Link } from "react-router";
import {
Breadcrumb,
BreadcrumbItem,
@@ -17,7 +18,7 @@ import {
import { AppSidebar } from "@/features/sidebar";
import { Intergration } from "@/features/integration/Integration";
-export function IntegrationPage(): React.ReactElement {
+export function IntegrationSettingsPage(): React.ReactElement {
return (
@@ -31,7 +32,15 @@ export function IntegrationPage(): React.ReactElement {
- /
+
+ /
+
+
+
+
+
+ Settings
+
diff --git a/apps/client/src/pages/log/index.tsx b/apps/client/src/pages/settings/log.tsx
similarity index 73%
rename from apps/client/src/pages/log/index.tsx
rename to apps/client/src/pages/settings/log.tsx
index 70c57c1..0bada1f 100644
--- a/apps/client/src/pages/log/index.tsx
+++ b/apps/client/src/pages/settings/log.tsx
@@ -1,3 +1,4 @@
+import { Link } from "react-router";
import {
Breadcrumb,
BreadcrumbItem,
@@ -15,7 +16,7 @@ import {
import { Logs } from "@/features/log";
import { AppSidebar } from "@/features/sidebar";
-export function LogPage() {
+export function LogSettingsPage() {
return (
@@ -29,7 +30,15 @@ export function LogPage() {
- /
+
+ /
+
+
+
+
+
+ Settings
+
diff --git a/apps/client/src/pages/networks/index.tsx b/apps/client/src/pages/settings/networks.tsx
similarity index 89%
rename from apps/client/src/pages/networks/index.tsx
rename to apps/client/src/pages/settings/networks.tsx
index 7a1456b..700086f 100644
--- a/apps/client/src/pages/networks/index.tsx
+++ b/apps/client/src/pages/settings/networks.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
+import { Link } from "react-router";
import {
Breadcrumb,
BreadcrumbItem,
@@ -60,7 +61,7 @@ function NetworkCard({ icon, name, status }: NetworkCardProps) {
);
}
-export function NetworksPage() {
+export function NetworksSettingsPage() {
const isOnline = useOnlineStatus();
return (
@@ -76,7 +77,15 @@ export function NetworksPage() {
- /
+
+ /
+
+
+
+
+
+ Settings
+
diff --git a/apps/client/src/pages/settings/services.tsx b/apps/client/src/pages/settings/services.tsx
new file mode 100644
index 0000000..47651df
--- /dev/null
+++ b/apps/client/src/pages/settings/services.tsx
@@ -0,0 +1,269 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router";
+import { toast } from "sonner";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
+import { AppSidebar } from "@/features/sidebar";
+import { Separator } from "@/components/ui/separator";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { useTunnelStore } from "@/entities/tunnel/store";
+import { useSupabaseAuth } from "@/contexts/SupabaseAuthContext";
+import { useConfigStore } from "@/entities/configurations/store";
+import {
+ CODE_SERVICE_CONFIG_KEY,
+ getCodeServiceEnabled,
+} from "@/entities/configurations/codeService";
+
+export function ServicesSettingsPage() {
+ const { status, isLoading, error, refresh, start, stop } = useTunnelStore();
+ const { session } = useSupabaseAuth();
+ const [server, setServer] = useState(
+ "wss://agent.tunnel.cartesiancs.com/agent",
+ );
+ const [target, setTarget] = useState("http://127.0.0.1:6174");
+ const [localError, setLocalError] = useState(null);
+
+ const {
+ configurations,
+ fetchConfigs,
+ createConfig,
+ updateConfig,
+ isLoading: configsLoading,
+ } = useConfigStore();
+ const codeConfigRow = configurations.find(
+ (c) => c.key === CODE_SERVICE_CONFIG_KEY,
+ );
+ const codeServiceEnabled = getCodeServiceEnabled(configurations);
+
+ useEffect(() => {
+ void fetchConfigs();
+ }, [fetchConfigs]);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ useEffect(() => {
+ if (status.server) setServer(status.server);
+ if (status.target) setTarget(status.target);
+ }, [status.server, status.target]);
+
+ const handleCodeServiceToggle = async (on: boolean) => {
+ try {
+ if (codeConfigRow) {
+ await updateConfig(codeConfigRow.id, {
+ key: codeConfigRow.key,
+ value: codeConfigRow.value,
+ enabled: on ? 1 : 0,
+ description: codeConfigRow.description,
+ });
+ } else if (on) {
+ await createConfig({
+ key: CODE_SERVICE_CONFIG_KEY,
+ value: "1",
+ enabled: 1,
+ description: "Enable the Code workspace (file browser and editor).",
+ });
+ }
+ toast.success(on ? "Code workspace enabled" : "Code workspace disabled");
+ } catch {
+ toast.error("Failed to update Code workspace setting");
+ }
+ };
+
+ const handleToggle = async (checked: boolean) => {
+ setLocalError(null);
+ if (checked) {
+ if (!server || !target) {
+ setLocalError("Enter tunnel server URL and target.");
+ return;
+ }
+ try {
+ await start(server, target, session?.access_token);
+ } catch (err) {
+ setLocalError(
+ err instanceof Error ? err.message : "Failed to start tunnel.",
+ );
+ }
+ } else {
+ try {
+ await stop();
+ } catch (err) {
+ setLocalError(
+ err instanceof Error ? err.message : "Failed to stop tunnel.",
+ );
+ }
+ }
+ };
+
+ const copySession = async () => {
+ if (status.session_id) {
+ const url = `https://${status.session_id}.tunnel.cartesiancs.com/auth`;
+ await navigator.clipboard.writeText(url);
+ toast.success("Tunnel URL copied", {
+ description: url,
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+ Settings
+
+
+
+
+ Services
+
+
+
+
+
+
+
+
Services
+
+
+
+
+ Code workspace
+
+
+
+
+
+ Code service
+
+
void handleCodeServiceToggle(v)}
+ />
+
+
+
+
+
+
+ Tunnel
+
+
+
+
+
+ Tunnel Connection
+
+
+
+
+
+
+
+
+ Session
+ {status.session_id ? (
+
+ {status.session_id}
+
+ ) : (
+ None
+ )}
+
+ {status.session_id && (
+
+
+ Copy tunnel URL
+
+
+ )}
+
+
+
+
+ Refresh status
+
+ {(error || localError) && (
+
+ {localError ?? error}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/client/src/pages/users/index.tsx b/apps/client/src/pages/settings/users.tsx
similarity index 79%
rename from apps/client/src/pages/users/index.tsx
rename to apps/client/src/pages/settings/users.tsx
index 72f8cd3..33e5da0 100644
--- a/apps/client/src/pages/users/index.tsx
+++ b/apps/client/src/pages/settings/users.tsx
@@ -1,3 +1,4 @@
+import { Link } from "react-router";
import {
Breadcrumb,
BreadcrumbItem,
@@ -17,7 +18,7 @@ import { AppSidebar } from "@/features/sidebar";
import { RoleTable } from "@/widgets/role-table/RoleList";
import { UserTable } from "@/widgets/user-table/UserList";
-export function UsersPage() {
+export function UsersSettingsPage() {
return (
@@ -31,7 +32,15 @@ export function UsersPage() {
- /
+
+ /
+
+
+
+
+
+ Settings
+
diff --git a/apps/client/src/widgets/device-list/DeviceList.tsx b/apps/client/src/widgets/device-list/DeviceList.tsx
index b0a3138..7d87086 100644
--- a/apps/client/src/widgets/device-list/DeviceList.tsx
+++ b/apps/client/src/widgets/device-list/DeviceList.tsx
@@ -19,6 +19,7 @@ import { useDeviceStore } from "@/entities/device/store";
import { DeviceDeleteButton } from "@/features/device/DeviceDeleteButton";
import { DeviceUpdateButton } from "@/features/device/DeviceUpdateButton";
import { DeviceCreateButton } from "@/features/device/DeviceCreateButton";
+import { DeviceKeyButton } from "@/features/device/DeviceKeyButton";
import {
DropdownMenu,
DropdownMenuContent,
@@ -105,6 +106,7 @@ export function DeviceList() {
+
diff --git a/apps/client/tsconfig.app.tsbuildinfo b/apps/client/tsconfig.app.tsbuildinfo
index 38af732..39de0cc 100644
--- a/apps/client/tsconfig.app.tsbuildinfo
+++ b/apps/client/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/app/pagewrapper/page-wrapper.tsx","./src/app/providers/theme-provider.tsx","./src/components/icon/logo.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/popover.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/contexts/supabaseauthcontext.tsx","./src/entities/configurations/api.ts","./src/entities/configurations/codeservice.ts","./src/entities/configurations/store.ts","./src/entities/configurations/types.ts","./src/entities/custom-nodes/api.ts","./src/entities/custom-nodes/presets.ts","./src/entities/custom-nodes/store.ts","./src/entities/custom-nodes/types.ts","./src/entities/device/api.ts","./src/entities/device/store.ts","./src/entities/device/types.ts","./src/entities/device-token/api.ts","./src/entities/device-token/store.ts","./src/entities/device-token/types.ts","./src/entities/dynamic-dashboard/api.ts","./src/entities/dynamic-dashboard/interaction.ts","./src/entities/dynamic-dashboard/layoutresolve.ts","./src/entities/dynamic-dashboard/store.ts","./src/entities/entity/api.ts","./src/entities/entity/store.ts","./src/entities/entity/types.ts","./src/entities/file/api.ts","./src/entities/file/store.ts","./src/entities/file/types.ts","./src/entities/flow/api.ts","./src/entities/flow/store.ts","./src/entities/flow/types.ts","./src/entities/ha/api.ts","./src/entities/ha/store.ts","./src/entities/ha/types.ts","./src/entities/integrations/api.ts","./src/entities/integrations/store.ts","./src/entities/integrations/types.ts","./src/entities/log/api.ts","./src/entities/log/types.ts","./src/entities/map/api.ts","./src/entities/map/store.ts","./src/entities/map/types.ts","./src/entities/permission/api.ts","./src/entities/permission/store.ts","./src/entities/permission/types.ts","./src/entities/recording/api.ts","./src/entities/recording/index.ts","./src/entities/recording/store.ts","./src/entities/recording/types.ts","./src/entities/role/api.ts","./src/entities/role/store.ts","./src/entities/role/types.ts","./src/entities/stat/api.ts","./src/entities/stat/store.ts","./src/entities/stat/types.ts","./src/entities/tunnel/api.ts","./src/entities/tunnel/store.ts","./src/entities/tunnel/types.ts","./src/entities/user/api.ts","./src/entities/user/store.ts","./src/entities/user/types.ts","./src/features/account-switcher/index.tsx","./src/features/auth/authinterceptor.tsx","./src/features/auth/defaultadminpassworddialog.tsx","./src/features/auth/api.ts","./src/features/auth/hook.ts","./src/features/auth/index.tsx","./src/features/code/createitemdialog.tsx","./src/features/code/fileeditor.tsx","./src/features/code/filetree.tsx","./src/features/configurations/configurationactionbutton.tsx","./src/features/configurations/configurationcreate.tsx","./src/features/configurations/configurationcreatebutton.tsx","./src/features/darkmode/mode-toggle.tsx","./src/features/dashboard-swipe/dashboardswipeheader.tsx","./src/features/dashboard-swipe/dashboardswipelayout.tsx","./src/features/device/devicecreatebutton.tsx","./src/features/device/devicedeletebutton.tsx","./src/features/device/deviceupdatebutton.tsx","./src/features/device-token/devicetokenmanager.tsx","./src/features/dynamic-dashboard/groupcanvas.tsx","./src/features/dynamic-dashboard/events/dispatcher.test.ts","./src/features/dynamic-dashboard/events/dispatcher.ts","./src/features/dynamic-dashboard/panels/buttonpanel.tsx","./src/features/dynamic-dashboard/panels/flowpanel.tsx","./src/features/dynamic-dashboard/panels/mappanel.tsx","./src/features/entity/allentities.tsx","./src/features/entity/analyzemenuitem.tsx","./src/features/entity/card.tsx","./src/features/entity/entitycreatebutton.tsx","./src/features/entity/entitydeletebutton.tsx","./src/features/entity/entityupdatebutton.tsx","./src/features/entity/selectplatforms.tsx","./src/features/entity/selecttypes.tsx","./src/features/entity/useentitiesdata.ts","./src/features/error/index.tsx","./src/features/flow/addcustomnode.tsx","./src/features/flow/flow.tsx","./src/features/flow/graph.tsx","./src/features/flow/options.tsx","./src/features/flow/optionsvariation.tsx","./src/features/flow/runflow.tsx","./src/features/flow/selecteditemactions.tsx","./src/features/flow/flownode.ts","./src/features/flow/flowtypes.ts","./src/features/flow/flowutils.ts","./src/features/flow/flow-chat/buildsystemprompt.ts","./src/features/flow/flow-chat/executetoolcalls.ts","./src/features/flow/flow-chat/flowtools.ts","./src/features/flow/flow-chat/index.ts","./src/features/flow/nodes/buttonnode.tsx","./src/features/flow/nodes/calcnode.tsx","./src/features/flow/nodes/httpnode.tsx","./src/features/flow/nodes/intervalnode.tsx","./src/features/flow/nodes/logicnode.tsx","./src/features/flow/nodes/loopnode.tsx","./src/features/flow/nodes/mqttnode.tsx","./src/features/flow/nodes/numbernode.tsx","./src/features/flow/nodes/processingnode.tsx","./src/features/flow/nodes/titlenode.tsx","./src/features/flow/nodes/varnode.tsx","./src/features/flow-log/flowlog.tsx","./src/features/footer/index.tsx","./src/features/gps/parsegps.ts","./src/features/ha/haentitiestable.tsx","./src/features/ha/hastatblock.tsx","./src/features/ha/index.tsx","./src/features/integration/ha.tsx","./src/features/integration/integration.tsx","./src/features/integration/ros.tsx","./src/features/integration/sdr.tsx","./src/features/integration/constants.ts","./src/features/integration/types.ts","./src/features/json/jsoneditor.tsx","./src/features/llm-chat/chatinput.tsx","./src/features/llm-chat/chatmessage.tsx","./src/features/llm-chat/chatmessages.tsx","./src/features/llm-chat/chatpanel.tsx","./src/features/llm-chat/chatpanelcontainer.tsx","./src/features/llm-chat/chatpanelmobile.tsx","./src/features/llm-chat/index.tsx","./src/features/llm-chat/store.ts","./src/features/llm-chat/types.ts","./src/features/llm-chat/usechatkeyboard.ts","./src/features/log/index.tsx","./src/features/map/currentlocationmarker.tsx","./src/features/map/mapviewpersistence.tsx","./src/features/map/index.tsx","./src/features/map-draw/featuredetailspanel.tsx","./src/features/map-draw/featuredrawingpreview.tsx","./src/features/map-draw/featureeditor.tsx","./src/features/map-draw/featurerenderer.tsx","./src/features/map-draw/layerdialog.tsx","./src/features/map-draw/layersidebar.tsx","./src/features/map-draw/mapevents.tsx","./src/features/map-draw/maptoolbar.tsx","./src/features/map-entity/entitydetailspanel.tsx","./src/features/map-entity/render.tsx","./src/features/map-entity/store.ts","./src/features/recording/recordingbutton.tsx","./src/features/recording/recordingslist.tsx","./src/features/recording/videoplaybackdialog.tsx","./src/features/recording/index.ts","./src/features/recording/components/audiowaveformplayer.tsx","./src/features/recording/components/frametimeline.tsx","./src/features/recording/components/playbackcontrols.tsx","./src/features/recording/components/timeruler.tsx","./src/features/recording/components/videocontrolbar.tsx","./src/features/recording/components/waveformcanvas.tsx","./src/features/recording/hooks/useaudiowaveform.ts","./src/features/recording/hooks/usemediaplayback.ts","./src/features/recording/hooks/usevideoframes.ts","./src/features/role/roledialogs.tsx","./src/features/role/roleform.tsx","./src/features/ros2/ros2dashboard.tsx","./src/features/rtc/audiolevelbar.tsx","./src/features/rtc/streamreceiver.tsx","./src/features/rtc/webrtcprovider.tsx","./src/features/rtc/captureframe.ts","./src/features/rtc/rtc.ts","./src/features/rtc/turnservice.ts","./src/features/sdr/sdraudioplayer.tsx","./src/features/sdr/sdrdashboard.tsx","./src/features/sdr/api.ts","./src/features/search/search-form.tsx","./src/features/server-resource/resourceusage.tsx","./src/features/setup/index.tsx","./src/features/sidebar/footer.tsx","./src/features/sidebar/index.tsx","./src/features/stat/index.tsx","./src/features/topbar/index.tsx","./src/features/user/userroleassigner.tsx","./src/features/user/useradd.tsx","./src/features/user/userdelete.tsx","./src/features/user/useredit.tsx","./src/features/user/userform.tsx","./src/features/ws/flowuieventbridge.tsx","./src/features/ws/isconnected.tsx","./src/features/ws/websocketprovider.tsx","./src/features/ws/flowuieventrouter.test.ts","./src/features/ws/flowuieventrouter.ts","./src/features/ws/ws.ts","./src/features/ws/wsmock.ts","./src/features/ws/flowuiadapters/toastflowuiadapter.ts","./src/hooks/use-mobile.ts","./src/hooks/usedesktopsidecar.ts","./src/hooks/usepreventbacknavigation.ts","./src/lib/electron.ts","./src/lib/geometry-precision.ts","./src/lib/geometry.ts","./src/lib/jwt.ts","./src/lib/storage.ts","./src/lib/string.ts","./src/lib/supabase.ts","./src/lib/time.ts","./src/lib/utils.ts","./src/pages/auth/index.tsx","./src/pages/code/index.tsx","./src/pages/dashboard/dashboardmainpanel.tsx","./src/pages/dashboard/index.tsx","./src/pages/desktop-settings/index.tsx","./src/pages/devices/index.tsx","./src/pages/dynamic-dashboard/dynamicdashboardmainpanel.tsx","./src/pages/dynamic-dashboard/newdynamicdashboardpanel.tsx","./src/pages/dynamic-dashboard/index.tsx","./src/pages/flow/index.tsx","./src/pages/integration/index.tsx","./src/pages/key/index.tsx","./src/pages/landing/index.tsx","./src/pages/log/index.tsx","./src/pages/map/index.tsx","./src/pages/networks/index.tsx","./src/pages/notfound/index.tsx","./src/pages/recordings/index.tsx","./src/pages/servers/index.tsx","./src/pages/settings/index.tsx","./src/pages/setup/index.tsx","./src/pages/users/index.tsx","./src/shared/demo.ts","./src/shared/desktop.ts","./src/shared/api/index.ts","./src/shared/mock/mockadapter.ts","./src/shared/mock/mockdata.ts","./src/widgets/auth/authenticatedlayout.tsx","./src/widgets/auth/topbarwrapper.tsx","./src/widgets/device-list/devicelist.tsx","./src/widgets/entity-list/entitylist.tsx","./src/widgets/role-table/rolelist.tsx","./src/widgets/user-table/userlist.tsx"],"errors":true,"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/app/pagewrapper/page-wrapper.tsx","./src/app/providers/theme-provider.tsx","./src/components/icon/logo.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/popover.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/contexts/supabaseauthcontext.tsx","./src/entities/configurations/api.ts","./src/entities/configurations/codeservice.ts","./src/entities/configurations/store.ts","./src/entities/configurations/types.ts","./src/entities/custom-nodes/api.ts","./src/entities/custom-nodes/presets.ts","./src/entities/custom-nodes/store.ts","./src/entities/custom-nodes/types.ts","./src/entities/device/api.ts","./src/entities/device/store.ts","./src/entities/device/types.ts","./src/entities/device-token/api.ts","./src/entities/device-token/store.ts","./src/entities/device-token/types.ts","./src/entities/dynamic-dashboard/api.ts","./src/entities/dynamic-dashboard/interaction.ts","./src/entities/dynamic-dashboard/layoutresolve.ts","./src/entities/dynamic-dashboard/store.ts","./src/entities/entity/api.ts","./src/entities/entity/store.ts","./src/entities/entity/types.ts","./src/entities/file/api.ts","./src/entities/file/store.ts","./src/entities/file/types.ts","./src/entities/flow/api.ts","./src/entities/flow/store.ts","./src/entities/flow/types.ts","./src/entities/ha/api.ts","./src/entities/ha/store.ts","./src/entities/ha/types.ts","./src/entities/integrations/api.ts","./src/entities/integrations/store.ts","./src/entities/integrations/types.ts","./src/entities/log/api.ts","./src/entities/log/types.ts","./src/entities/map/api.ts","./src/entities/map/store.ts","./src/entities/map/types.ts","./src/entities/permission/api.ts","./src/entities/permission/store.ts","./src/entities/permission/types.ts","./src/entities/recording/api.ts","./src/entities/recording/index.ts","./src/entities/recording/store.ts","./src/entities/recording/types.ts","./src/entities/role/api.ts","./src/entities/role/store.ts","./src/entities/role/types.ts","./src/entities/stat/api.ts","./src/entities/stat/store.ts","./src/entities/stat/types.ts","./src/entities/tunnel/api.ts","./src/entities/tunnel/store.ts","./src/entities/tunnel/types.ts","./src/entities/user/api.ts","./src/entities/user/store.ts","./src/entities/user/types.ts","./src/features/account-switcher/index.tsx","./src/features/auth/authinterceptor.tsx","./src/features/auth/defaultadminpassworddialog.tsx","./src/features/auth/api.ts","./src/features/auth/hook.ts","./src/features/auth/index.tsx","./src/features/code/createitemdialog.tsx","./src/features/code/fileeditor.tsx","./src/features/code/filetree.tsx","./src/features/configurations/configurationactionbutton.tsx","./src/features/configurations/configurationcreate.tsx","./src/features/configurations/configurationcreatebutton.tsx","./src/features/darkmode/mode-toggle.tsx","./src/features/dashboard-swipe/dashboardswipeheader.tsx","./src/features/dashboard-swipe/dashboardswipelayout.tsx","./src/features/device/devicecreatebutton.tsx","./src/features/device/devicedeletebutton.tsx","./src/features/device/devicekeybutton.tsx","./src/features/device/deviceupdatebutton.tsx","./src/features/device-token/devicetokenmanager.tsx","./src/features/dynamic-dashboard/groupcanvas.tsx","./src/features/dynamic-dashboard/events/dispatcher.test.ts","./src/features/dynamic-dashboard/events/dispatcher.ts","./src/features/dynamic-dashboard/panels/buttonpanel.tsx","./src/features/dynamic-dashboard/panels/flowpanel.tsx","./src/features/dynamic-dashboard/panels/mappanel.tsx","./src/features/entity/allentities.tsx","./src/features/entity/analyzemenuitem.tsx","./src/features/entity/card.tsx","./src/features/entity/entitycreatebutton.tsx","./src/features/entity/entitydeletebutton.tsx","./src/features/entity/entityupdatebutton.tsx","./src/features/entity/selectplatforms.tsx","./src/features/entity/selecttypes.tsx","./src/features/entity/statehistorysheet.tsx","./src/features/entity/useentitiesdata.ts","./src/features/error/index.tsx","./src/features/flow/addcustomnode.tsx","./src/features/flow/flow.tsx","./src/features/flow/graph.tsx","./src/features/flow/options.tsx","./src/features/flow/optionsvariation.tsx","./src/features/flow/runflow.tsx","./src/features/flow/selecteditemactions.tsx","./src/features/flow/flownode.ts","./src/features/flow/flowtypes.ts","./src/features/flow/flowutils.ts","./src/features/flow/flow-chat/buildsystemprompt.ts","./src/features/flow/flow-chat/executetoolcalls.ts","./src/features/flow/flow-chat/flowtools.ts","./src/features/flow/flow-chat/index.ts","./src/features/flow/nodes/buttonnode.tsx","./src/features/flow/nodes/calcnode.tsx","./src/features/flow/nodes/httpnode.tsx","./src/features/flow/nodes/intervalnode.tsx","./src/features/flow/nodes/logicnode.tsx","./src/features/flow/nodes/loopnode.tsx","./src/features/flow/nodes/mqttnode.tsx","./src/features/flow/nodes/numbernode.tsx","./src/features/flow/nodes/processingnode.tsx","./src/features/flow/nodes/titlenode.tsx","./src/features/flow/nodes/varnode.tsx","./src/features/flow-log/flowlog.tsx","./src/features/footer/index.tsx","./src/features/gps/parsegps.ts","./src/features/ha/haentitiestable.tsx","./src/features/ha/hastatblock.tsx","./src/features/ha/index.tsx","./src/features/integration/ha.tsx","./src/features/integration/integration.tsx","./src/features/integration/ros.tsx","./src/features/integration/sdr.tsx","./src/features/integration/constants.ts","./src/features/integration/types.ts","./src/features/json/jsoneditor.tsx","./src/features/llm-chat/chatinput.tsx","./src/features/llm-chat/chatmessage.tsx","./src/features/llm-chat/chatmessages.tsx","./src/features/llm-chat/chatpanel.tsx","./src/features/llm-chat/chatpanelcontainer.tsx","./src/features/llm-chat/chatpanelmobile.tsx","./src/features/llm-chat/index.tsx","./src/features/llm-chat/store.ts","./src/features/llm-chat/types.ts","./src/features/llm-chat/usechatkeyboard.ts","./src/features/log/index.tsx","./src/features/map/currentlocationmarker.tsx","./src/features/map/mapviewpersistence.tsx","./src/features/map/index.tsx","./src/features/map-draw/featuredetailspanel.tsx","./src/features/map-draw/featuredrawingpreview.tsx","./src/features/map-draw/featureeditor.tsx","./src/features/map-draw/featurerenderer.tsx","./src/features/map-draw/layerdialog.tsx","./src/features/map-draw/layersidebar.tsx","./src/features/map-draw/mapevents.tsx","./src/features/map-draw/maptoolbar.tsx","./src/features/map-entity/entitydetailspanel.tsx","./src/features/map-entity/render.tsx","./src/features/map-entity/store.ts","./src/features/recording/recordingbutton.tsx","./src/features/recording/recordingslist.tsx","./src/features/recording/videoplaybackdialog.tsx","./src/features/recording/index.ts","./src/features/recording/components/audiowaveformplayer.tsx","./src/features/recording/components/frametimeline.tsx","./src/features/recording/components/playbackcontrols.tsx","./src/features/recording/components/timeruler.tsx","./src/features/recording/components/videocontrolbar.tsx","./src/features/recording/components/waveformcanvas.tsx","./src/features/recording/hooks/useaudiowaveform.ts","./src/features/recording/hooks/usemediaplayback.ts","./src/features/recording/hooks/usevideoframes.ts","./src/features/role/roledialogs.tsx","./src/features/role/roleform.tsx","./src/features/ros2/ros2dashboard.tsx","./src/features/rtc/audiolevelbar.tsx","./src/features/rtc/streamreceiver.tsx","./src/features/rtc/webrtcprovider.tsx","./src/features/rtc/captureframe.ts","./src/features/rtc/rtc.ts","./src/features/rtc/turnservice.ts","./src/features/sdr/sdraudioplayer.tsx","./src/features/sdr/sdrdashboard.tsx","./src/features/sdr/api.ts","./src/features/search/search-form.tsx","./src/features/server-resource/resourceusage.tsx","./src/features/setup/index.tsx","./src/features/sidebar/footer.tsx","./src/features/sidebar/index.tsx","./src/features/stat/index.tsx","./src/features/topbar/index.tsx","./src/features/user/userroleassigner.tsx","./src/features/user/useradd.tsx","./src/features/user/userdelete.tsx","./src/features/user/useredit.tsx","./src/features/user/userform.tsx","./src/features/ws/flowuieventbridge.tsx","./src/features/ws/isconnected.tsx","./src/features/ws/websocketprovider.tsx","./src/features/ws/flowuieventrouter.test.ts","./src/features/ws/flowuieventrouter.ts","./src/features/ws/ws.ts","./src/features/ws/wsmock.ts","./src/features/ws/flowuiadapters/toastflowuiadapter.ts","./src/hooks/use-mobile.ts","./src/hooks/usedesktopsidecar.ts","./src/hooks/usepreventbacknavigation.ts","./src/lib/electron.ts","./src/lib/geometry-precision.ts","./src/lib/geometry.ts","./src/lib/jwt.ts","./src/lib/storage.ts","./src/lib/string.ts","./src/lib/supabase.ts","./src/lib/time.ts","./src/lib/utils.ts","./src/pages/auth/index.tsx","./src/pages/code/index.tsx","./src/pages/dashboard/dashboardmainpanel.tsx","./src/pages/dashboard/index.tsx","./src/pages/desktop-settings/index.tsx","./src/pages/devices/index.tsx","./src/pages/dynamic-dashboard/dynamicdashboardmainpanel.tsx","./src/pages/dynamic-dashboard/newdynamicdashboardpanel.tsx","./src/pages/dynamic-dashboard/index.tsx","./src/pages/flow/index.tsx","./src/pages/landing/index.tsx","./src/pages/map/index.tsx","./src/pages/notfound/index.tsx","./src/pages/recordings/index.tsx","./src/pages/settings/account.tsx","./src/pages/settings/config.tsx","./src/pages/settings/index.tsx","./src/pages/settings/integration.tsx","./src/pages/settings/log.tsx","./src/pages/settings/networks.tsx","./src/pages/settings/services.tsx","./src/pages/settings/users.tsx","./src/pages/setup/index.tsx","./src/shared/demo.ts","./src/shared/desktop.ts","./src/shared/api/index.ts","./src/shared/mock/mockadapter.ts","./src/shared/mock/mockdata.ts","./src/widgets/auth/authenticatedlayout.tsx","./src/widgets/auth/topbarwrapper.tsx","./src/widgets/device-list/devicelist.tsx","./src/widgets/entity-list/entitylist.tsx","./src/widgets/role-table/rolelist.tsx","./src/widgets/user-table/userlist.tsx"],"errors":true,"version":"5.8.3"}
\ No newline at end of file
diff --git a/apps/server/src/db/repository/mod.rs b/apps/server/src/db/repository/mod.rs
index 3b0b36b..de36de6 100644
--- a/apps/server/src/db/repository/mod.rs
+++ b/apps/server/src/db/repository/mod.rs
@@ -739,6 +739,33 @@ pub fn set_entity_state(
})
}
+pub fn get_entity_state_history(
+ pool: &DbPool,
+ target_entity_id: &str,
+) -> Result, anyhow::Error> {
+ use crate::db::schema::states::dsl as s;
+ use crate::db::schema::states_meta::dsl as sm;
+
+ let mut conn = pool.get()?;
+
+ let metadata_id_opt: Option = sm::states_meta
+ .filter(sm::entity_id.eq(target_entity_id))
+ .select(sm::metadata_id)
+ .first::(&mut conn)
+ .optional()?;
+
+ let Some(meta_id) = metadata_id_opt else {
+ return Ok(Vec::new());
+ };
+
+ let history = s::states
+ .filter(s::metadata_id.eq(meta_id))
+ .order(s::last_updated.asc())
+ .load::(&mut conn)?;
+
+ Ok(history)
+}
+
pub fn get_all_entities_with_states_and_configs(
pool: &DbPool,
) -> Result, anyhow::Error> {
diff --git a/apps/server/src/handler/entities.rs b/apps/server/src/handler/entities.rs
index e49725b..4a53149 100644
--- a/apps/server/src/handler/entities.rs
+++ b/apps/server/src/handler/entities.rs
@@ -1,7 +1,7 @@
use crate::{
db::{
self,
- models::{EntityWithConfig, EntityWithStateAndConfig, NewEntity},
+ models::{EntityWithConfig, EntityWithStateAndConfig, NewEntity, State as EntityState},
},
error::AppError,
handler::auth::AuthUser,
@@ -142,3 +142,12 @@ pub async fn delete_entity(
json!({ "status": "success", "message": "Entity deleted" }),
))
}
+
+pub async fn get_entity_history(
+ State(state): State>,
+ AuthUser(_user): AuthUser,
+ Path(entity_id): Path,
+) -> Result>, AppError> {
+ let history = db::repository::get_entity_state_history(&state.pool, &entity_id)?;
+ Ok(Json(history))
+}
diff --git a/apps/server/src/routes.rs b/apps/server/src/routes.rs
index b910fe8..554572c 100644
--- a/apps/server/src/routes.rs
+++ b/apps/server/src/routes.rs
@@ -117,6 +117,10 @@ pub async fn web_server(
get(entities::get_entities).post(entities::create_entity),
)
.route("/entities/all", get(entities::get_entities_with_states))
+ .route(
+ "/entities/:entity_id/history",
+ get(entities::get_entity_history),
+ )
.route(
"/entities/:id",
put(entities::update_entity).delete(entities::delete_entity),