+ )
+}
diff --git a/ui/app/datastreams/page.tsx b/ui/app/datastreams/page.tsx
new file mode 100644
index 00000000..d013d78a
--- /dev/null
+++ b/ui/app/datastreams/page.tsx
@@ -0,0 +1,113 @@
+'use client'
+
+/*
+ * Copyright 2025 SUPSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Card } from '@heroui/card'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableColumn,
+ TableHeader,
+ TableRow,
+} from '@heroui/table'
+import * as React from 'react'
+
+import PhantomEditWarning from '@/components/PhantomEditWarning'
+import TemporalConflictWarning from '@/components/TemporalConflictWarning'
+import TemporalModeSwitch from '@/components/TemporalModeSwitch'
+import { useTemporalQuery } from '@/components/hooks/useTemporalQuery'
+
+import { useTemporal } from '@/context/TemporalContext'
+
+type Datastream = {
+ '@iot.id': number
+ name: string
+ description: string
+ observationType: string
+ phenomenonTime?: string
+ systemTimeValidity?: string
+}
+
+export default function DatastreamsPage() {
+ const { mode, asOf } = useTemporal()
+ const { data, loading, error, activeUrl } = useTemporalQuery<{
+ value: Datastream[]
+ }>('/api/datastreams')
+
+ const items = data?.value || []
+
+ return (
+
+
+
+
Datastreams
+
Inspect stream definitions and temporal validity with clear context.
+ )
+}
diff --git a/ui/components/PhantomEditWarning.tsx b/ui/components/PhantomEditWarning.tsx
new file mode 100644
index 00000000..df3167bb
--- /dev/null
+++ b/ui/components/PhantomEditWarning.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+/*
+ * Copyright 2025 SUPSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Card } from '@heroui/card'
+
+export default function PhantomEditWarning() {
+ return (
+
+ Note: Changes to phenomenonTime and observedArea are not versioned. The history below reflects only metadata and configuration changes.
+
+ )
+}
diff --git a/ui/components/TemporalBadge.tsx b/ui/components/TemporalBadge.tsx
new file mode 100644
index 00000000..12f982db
--- /dev/null
+++ b/ui/components/TemporalBadge.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+/*
+ * Copyright 2025 SUPSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Chip } from '@heroui/chip'
+import dayjs from 'dayjs'
+
+import { useTemporal } from '@/context/TemporalContext'
+
+export default function TemporalBadge() {
+ const { mode, asOf, fromTo, reset } = useTemporal()
+
+ if (mode === 'current') {
+ return (
+
+ ● Live
+
+ )
+ }
+
+ if (mode === 'as_of') {
+ const label = asOf ? dayjs(asOf).format('MMM D, YYYY, HH:mm') : 'Not set'
+ return (
+
+ ◷ As-of: {label}
+
+ )
+ }
+
+ const fromLabel = fromTo?.[0] ? dayjs(fromTo[0]).format('MMM D, YYYY HH:mm') : '-'
+ const toLabel = fromTo?.[1] ? dayjs(fromTo[1]).format('MMM D, YYYY HH:mm') : '-'
+ return (
+
+ ↔ {fromLabel} – {toLabel}
+
+ )
+}
diff --git a/ui/components/TemporalConflictWarning.tsx b/ui/components/TemporalConflictWarning.tsx
new file mode 100644
index 00000000..42f7a95d
--- /dev/null
+++ b/ui/components/TemporalConflictWarning.tsx
@@ -0,0 +1,31 @@
+'use client'
+
+/*
+ * Copyright 2025 SUPSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Card } from '@heroui/card'
+import dayjs from 'dayjs'
+
+type Props = {
+ asOf?: string | null
+}
+
+export default function TemporalConflictWarning({ asOf }: Props) {
+ return (
+
+ This entity did not exist at {asOf ? dayjs(asOf).format('MMM D, YYYY, HH:mm') : 'the selected time'}. Try a more recent timestamp.
+
+ )
+}
diff --git a/ui/components/TemporalModeSwitch.tsx b/ui/components/TemporalModeSwitch.tsx
new file mode 100644
index 00000000..3d03efe9
--- /dev/null
+++ b/ui/components/TemporalModeSwitch.tsx
@@ -0,0 +1,121 @@
+'use client'
+
+/*
+ * Copyright 2025 SUPSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Input } from '@heroui/input'
+import { Tab, Tabs } from '@heroui/tabs'
+import React from 'react'
+
+import { useTemporal } from '@/context/TemporalContext'
+
+function toLocalInputValue(value: string | null) {
+ if (!value) return ''
+ const date = new Date(value)
+ const tzOffset = date.getTimezoneOffset() * 60000
+ return new Date(date.getTime() - tzOffset).toISOString().slice(0, 16)
+}
+
+function fromLocalInputValue(value: string) {
+ if (!value) return null
+ return new Date(value).toISOString()
+}
+
+export default function TemporalModeSwitch() {
+ const { mode, asOf, fromTo, setMode, setAsOf, setFromTo } = useTemporal()
+
+ return (
+
+ {t(
+ 'wizard.mode_switch_warning_description',
+ 'Data entered in this mode stays here and is not automatically transferred to the other mode.'
+ )}
+
+
+ )
+}
diff --git a/ui/features/history/components/HistoryDetails.tsx b/ui/features/history/components/HistoryDetails.tsx
new file mode 100644
index 00000000..285ddaa1
--- /dev/null
+++ b/ui/features/history/components/HistoryDetails.tsx
@@ -0,0 +1,109 @@
+'use client'
+
+// Copyright 2026 SUPSI
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Card } from '@heroui/card'
+import { Chip } from '@heroui/chip'
+import dayjs from 'dayjs'
+
+import { HistoryResponse } from '@/features/history/types'
+
+const actionColorMap = {
+ CREATE: 'success',
+ UPDATE: 'warning',
+ DELETE: 'danger',
+} as const
+
+export default function HistoryDetails({
+ data,
+ loading,
+}: {
+ data: HistoryResponse | null
+ loading: boolean
+}) {
+ if (loading) {
+ return