From 96a4b4a9100a48a5fbcefb6f49676af2f648d722 Mon Sep 17 00:00:00 2001 From: DipokalLab Date: Mon, 27 Apr 2026 00:29:18 +0900 Subject: [PATCH 1/7] build: add vite env --- .github/workflows/release-macos.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index b6635cd..f2a0d5b 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -65,6 +65,9 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_ENTITLEMENTS_PATH: ${{ github.workspace }}/apps/desktop/src-tauri/entitlements.plist + VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} + VITE_CAPSULE_URL: ${{ secrets.VITE_CAPSULE_URL }} working-directory: apps/desktop run: npm run tauri -- build --target aarch64-apple-darwin From 7788b814586ced8c720544cb7de0e12eef3c664b Mon Sep 17 00:00:00 2001 From: DipokalLab Date: Mon, 27 Apr 2026 14:44:22 +0900 Subject: [PATCH 2/7] fix: remove supabase env --- apps/client/src/lib/supabase.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/client/src/lib/supabase.ts b/apps/client/src/lib/supabase.ts index 3e06b2f..904e49d 100644 --- a/apps/client/src/lib/supabase.ts +++ b/apps/client/src/lib/supabase.ts @@ -3,4 +3,16 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; -export const supabase = createClient(supabaseUrl, supabaseAnonKey); +export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseAnonKey); + +if (!isSupabaseConfigured) { + console.warn( + "[supabase] VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY is not set. " + + "Supabase-dependent features (Google login, TURN credentials, edge functions) will be disabled.", + ); +} + +export const supabase = createClient( + supabaseUrl || "https://placeholder.supabase.co", + supabaseAnonKey || "placeholder-anon-key", +); From 98e52e06a646093e2a02d3742b778db03f1a9a17 Mon Sep 17 00:00:00 2001 From: DipokalLab Date: Mon, 27 Apr 2026 14:52:15 +0900 Subject: [PATCH 3/7] fix: remove key page --- apps/client/src/App.tsx | 9 -- .../device-token/DeviceTokenManager.tsx | 137 ++++++++---------- .../src/features/device/DeviceKeyButton.tsx | 40 +++++ apps/client/src/features/setup/index.tsx | 2 +- apps/client/src/features/sidebar/index.tsx | 6 - apps/client/src/pages/key/index.tsx | 106 -------------- .../src/widgets/device-list/DeviceList.tsx | 2 + 7 files changed, 104 insertions(+), 198 deletions(-) create mode 100644 apps/client/src/features/device/DeviceKeyButton.tsx delete mode 100644 apps/client/src/pages/key/index.tsx diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 5b769fc..1a47e4d 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -6,7 +6,6 @@ import { DashboardSwipeRoutePlaceholder, } from "./features/dashboard-swipe/DashboardSwipeLayout"; import { ServersPage } from "./pages/servers"; -import { KeyPage } from "./pages/key"; import { DevicePage } from "./pages/devices"; import { FlowPage } from "./pages/flow"; import { AuthInterceptor } from "./features/auth/AuthInterceptor"; @@ -76,14 +75,6 @@ const router = createBrowserRouter([ ), }, - { - path: "/key", - element: ( - - - - ), - }, { path: "/devices", element: ( diff --git a/apps/client/src/features/device-token/DeviceTokenManager.tsx b/apps/client/src/features/device-token/DeviceTokenManager.tsx index 9914c4f..6412ae9 100644 --- a/apps/client/src/features/device-token/DeviceTokenManager.tsx +++ b/apps/client/src/features/device-token/DeviceTokenManager.tsx @@ -1,13 +1,6 @@ import { useEffect, useState } from "react"; import { Copy, Check, Key, AlertTriangle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Dialog, DialogContent, @@ -61,78 +54,70 @@ export function DeviceTokenManager({ deviceId }: Props) { return ( <> - - - Access Token - - Manage the permanent access token for this device. - - - - {tokenInfo ? ( -
-
-

Token is active

-

- Created at: {new Date(tokenInfo.created_at).toLocaleString()} -

-
-
- - - - - - - - Are you sure? - - This will permanently revoke the access token. The - device will no longer be able to connect. - - - - Cancel - revokeToken(deviceId)}> - Continue - - - - -
+
+ {tokenInfo ? ( +
+
+

Token is active

+

+ Created at: {new Date(tokenInfo.created_at).toLocaleString()} +

- ) : ( -
-
-

- No active token -

-

- Issue a new token to allow this device to connect. -

-
- + + + + + + + Are you sure? + + This will permanently revoke the access token. The + device will no longer be able to connect. + + + + Cancel + revokeToken(deviceId)}> + Continue + + + + +
+
+ ) : ( +
+
+

+ No active token +

+

+ Issue a new token to allow this device to connect. +

- )} - - + +
+ )} +
clearNewToken()}> diff --git a/apps/client/src/features/device/DeviceKeyButton.tsx b/apps/client/src/features/device/DeviceKeyButton.tsx new file mode 100644 index 0000000..f30b549 --- /dev/null +++ b/apps/client/src/features/device/DeviceKeyButton.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Key } from "lucide-react"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { DeviceTokenManager } from "@/features/device-token/DeviceTokenManager"; + +interface Props { + deviceId: number; +} + +export function DeviceKeyButton({ deviceId }: Props) { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + e.preventDefault()}> + + Key + + + + + Access Token + + Manage the permanent access token for this device. + + + + + + ); +} diff --git a/apps/client/src/features/setup/index.tsx b/apps/client/src/features/setup/index.tsx index e11e43b..3b2de56 100644 --- a/apps/client/src/features/setup/index.tsx +++ b/apps/client/src/features/setup/index.tsx @@ -48,7 +48,7 @@ export const initialSetupSteps: SetupStep[] = [ title: "Add Sensor", description: "Add a sensor (entity) for the device.", isCompleted: false, - url: "/key", + url: "/devices", verifyStatus: async () => { try { const entity = await getAllEntities(); diff --git a/apps/client/src/features/sidebar/index.tsx b/apps/client/src/features/sidebar/index.tsx index 0383f95..1b49c9a 100644 --- a/apps/client/src/features/sidebar/index.tsx +++ b/apps/client/src/features/sidebar/index.tsx @@ -17,7 +17,6 @@ import { import { AccountSwitcher } from "../account-switcher"; import { useLocation, useNavigate } from "react-router"; import { - Key, LayoutDashboard, MonitorSmartphone, ScrollText, @@ -90,11 +89,6 @@ const data = { url: "/recordings", icon:
{!isTauriClient && recentUrls.length > 0 && !showAuthFields && ( From b81b0e6b2e837857acc696d1288f80b50f2d8c04 Mon Sep 17 00:00:00 2001 From: DipokalLab Date: Mon, 27 Apr 2026 21:56:42 +0900 Subject: [PATCH 7/7] feat: add history sheet --- apps/client/src/entities/entity/api.ts | 6 +- apps/client/src/features/entity/Card.tsx | 50 +++- .../src/features/entity/StateHistorySheet.tsx | 248 ++++++++++++++++++ apps/client/tsconfig.app.tsbuildinfo | 2 +- apps/server/src/db/repository/mod.rs | 27 ++ apps/server/src/handler/entities.rs | 11 +- apps/server/src/routes.rs | 4 + 7 files changed, 331 insertions(+), 17 deletions(-) create mode 100644 apps/client/src/features/entity/StateHistorySheet.tsx diff --git a/apps/client/src/entities/entity/api.ts b/apps/client/src/entities/entity/api.ts index 59e7dfe..7fbdb0c 100644 --- a/apps/client/src/entities/entity/api.ts +++ b/apps/client/src/entities/entity/api.ts @@ -1,8 +1,12 @@ import { apiClient } from "@/shared/api"; -import type { Entity, EntityAll, EntityPayload } from "./types"; +import type { Entity, EntityAll, EntityPayload, State } from "./types"; export const getEntities = () => apiClient.get("/entities"); export const getAllEntities = () => apiClient.get("/entities/all"); +export const getEntityHistory = (entityId: string) => + apiClient.get( + `/entities/${encodeURIComponent(entityId)}/history`, + ); export const getEntitiesFilter = (entityType?: string) => { return apiClient.get("/entities", { params: { diff --git a/apps/client/src/features/entity/Card.tsx b/apps/client/src/features/entity/Card.tsx index 02ab5d5..64ec3e4 100644 --- a/apps/client/src/features/entity/Card.tsx +++ b/apps/client/src/features/entity/Card.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { EntityAll } from "@/entities/entity/types"; import { Card, @@ -17,6 +18,7 @@ import { formatSimpleDateTime } from "@/lib/time"; import { StreamReceiver } from "../rtc/StreamReceiver"; import { RecordingMenuItem } from "../recording/RecordingButton"; import { AnalyzeMenuItem } from "./AnalyzeMenuItem"; +import { StateHistorySheet } from "./StateHistorySheet"; import { useNavigate } from "react-router"; type StreamState = { @@ -219,21 +221,41 @@ export function EntityCard({ ); } + return ; +} + +function DefaultEntityCard({ item }: { item: EntityAll }) { + const [historyOpen, setHistoryOpen] = useState(false); + return ( - - - {item.friendly_name} - - {item.state?.state || "N/A"} - - - -
{item.platform}
-
- {formatSimpleDateTime(item.state?.last_updated || "")} -
-
-
+ <> + + + {item.friendly_name} + + + +
{item.platform}
+
+ {formatSimpleDateTime(item.state?.last_updated || "")} +
+
+
+ + ); } diff --git a/apps/client/src/features/entity/StateHistorySheet.tsx b/apps/client/src/features/entity/StateHistorySheet.tsx new file mode 100644 index 0000000..b644eed --- /dev/null +++ b/apps/client/src/features/entity/StateHistorySheet.tsx @@ -0,0 +1,248 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { scaleTime } from "d3-scale"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Skeleton } from "@/components/ui/skeleton"; +import { formatSimpleDateTime } from "@/lib/time"; +import * as api from "@/entities/entity/api"; +import type { State } from "@/entities/entity/types"; + +type Props = { + entityId: string; + entityName: string; + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +const STRIP_WIDTH = 360; +const STRIP_HEIGHT = 80; +const STRIP_PADDING_X = 16; +const AXIS_Y = STRIP_HEIGHT - 24; + +export function StateHistorySheet({ + entityId, + entityName, + open, + onOpenChange, +}: Props) { + const [history, setHistory] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [now, setNow] = useState(() => new Date()); + const requestId = useRef(0); + + useEffect(() => { + if (!open) { + return; + } + const id = ++requestId.current; + setLoading(true); + setError(null); + setNow(new Date()); + + api + .getEntityHistory(entityId) + .then((res) => { + if (id !== requestId.current) return; + setHistory(res.data); + setLoading(false); + }) + .catch((err) => { + if (id !== requestId.current) return; + console.error("Failed to fetch entity history:", err); + setError("Failed to load history."); + setLoading(false); + }); + }, [open, entityId]); + + const sortedAsc = useMemo(() => { + if (!history) return []; + return [...history].sort( + (a, b) => + new Date(a.last_updated).getTime() - + new Date(b.last_updated).getTime(), + ); + }, [history]); + + const sortedDesc = useMemo(() => [...sortedAsc].reverse(), [sortedAsc]); + + const firstRecord = sortedAsc[0]; + const lastRecord = sortedAsc[sortedAsc.length - 1]; + + const xScale = useMemo(() => { + if (!firstRecord) return null; + const start = new Date(firstRecord.last_updated); + const end = now > new Date(lastRecord.last_updated) ? now : new Date(lastRecord.last_updated); + return scaleTime() + .domain([start, end]) + .range([STRIP_PADDING_X, STRIP_WIDTH - STRIP_PADDING_X]); + }, [firstRecord, lastRecord, now]); + + return ( + + + + {entityName || entityId} + + {entityId} + + + +
+ {loading && ( +
+ + + + +
+ )} + + {!loading && error && ( +
{error}
+ )} + + {!loading && !error && sortedAsc.length === 0 && ( +
+ No state history recorded yet. +
+ )} + + {!loading && !error && sortedAsc.length > 0 && xScale && ( + <> +
+ + + +
+ + + + + + first + + + now + + {sortedAsc.map((record) => { + const cx = xScale(new Date(record.last_updated)); + return ( + + + + + +
+
+ {formatSimpleDateTime(record.last_updated)} +
+
+ {record.state ?? "—"} +
+
+ + + ); + })} + {(() => { + const lastX = xScale(new Date(lastRecord!.last_updated)); + const nowX = xScale(now); + if (nowX > lastX + 6) { + return ( + + ); + } + return null; + })()} + +
+ + +
    + {sortedDesc.map((record) => ( +
  • + + {formatSimpleDateTime(record.last_updated)} + + + {record.state ?? "—"} + +
  • + ))} +
+
+ + )} +
+
+
+ ); +} + +function SummaryCell({ label, value }: { label: string; value: string }) { + return ( +
+ + {label} + + {value} +
+ ); +} diff --git a/apps/client/tsconfig.app.tsbuildinfo b/apps/client/tsconfig.app.tsbuildinfo index e360935..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/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/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 +{"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),