Skip to content

feat: add As-Of time-travel UI for viewing historical snapshots - #37

Open
bhudevbhanpuriya wants to merge 4 commits into
istSOS:GSoC_2026-TimeTravelfrom
bhudevbhanpuriya:feature/as-of-ui
Open

feat: add As-Of time-travel UI for viewing historical snapshots#37
bhudevbhanpuriya wants to merge 4 commits into
istSOS:GSoC_2026-TimeTravelfrom
bhudevbhanpuriya:feature/as-of-ui

Conversation

@bhudevbhanpuriya

Copy link
Copy Markdown

Introduces a full As-Of (time-travel) feature for the map/datastream UI, letting users view the SensorThings graph as it existed at a past point in time instead of live.

State & data layer:

  • AsOfContext/AsOfProvider (context/AsOfContext.tsx): global asOfDate/isSnapshot state, exposed via useAsOf().
  • asOfAdapter.ts: single AsOfAdapter interface (resolveThing, fetchCommits) with two implementations selected by NEXT_PUBLIC_VERSIONING_ENABLED:
    • mockAdapter: resolves state from local-dummy-things.ts, for demo/dev use with no backend versioning support.
    • apiAdapter: calls the real backend via Things(id)?$as_of= and Things(id)/Commit.
  • useAsOfThing / useAsOfCommits: hooks wrapping the adapter, exposing the resolved snapshot Thing, existence state (exists/not-yet-created/deleted), and commit history for a Thing.

UI components (features/as-of/components/):

  • AsOfNavbarButton + AsOfDropdown: navbar pill (Live / snapshot chip) and a calendar+time picker with quick-range presets, with a future-date guard.
  • AsOfBanner: page-wide banner shown while in snapshot mode.
  • TimelineScrubber: bottom-fixed slider across [earliest commit, now] with clickable commit tick marks.
  • AsOfExistenceDialog: dismissible floating alert with a "jump to nearest valid date" action when asOfDate falls outside a Thing's known lifetime.

Wiring into the app:

  • layout.tsx: wraps the app in AsOfProvider, renders AsOfBanner.
  • Navbar.tsx: adds AsOfNavbarButton, reworks the navbar into a 3-column grid to fit it.
  • Home.tsx: resolves the selected Thing's snapshot via useAsOfThing and feeds the resolved snapshotThing (not the live thing) into DatastreamTable; renders TimelineScrubber and AsOfExistenceDialog; re-arms the existence dialog only on a genuine transition into an invalid state, not on every scrubber tick.
  • DatastreamTable.tsx: read-only banner and hides Edit/Delete/New/ Import actions while isSnapshot is true.
  • LeafletMap.tsx / leafletDraw.ts / leafletCluster.ts: snapshot markers get an amber date-pill divIcon (createThingMarkerDivIcon); MapContextMenu hides "New"/"Import from file" during snapshot mode so the map stays read-only like the table.
  • ChartModal.tsx / ObservationGraph.tsx / observationGraphOptions.ts: clamps the date-range picker to asOfDate, draws a dashed snapshot marker line on the chart.
  • observations.ts: appends $as_of to the observations request when in snapshot mode.
  • useChartState.ts: namespaces the observation cache by asOfDate so live and snapshot results never collide.
  • useDataSourcesSync.ts: injects the local dummy stations onto the map only when NEXT_PUBLIC_VERSIONING_ENABLED is false (mock mode).

Known limitations

  • Production Docker build isn't a working target for this project yet
    (pre-existing, see README.GSoC.md TODO) - the NEXT_PUBLIC_VERSIONING_ENABLED
    fix in this PR only covers local dev (dev-docker-compose.yaml).
  • apiAdapter.fetchCommits only returns the backend's single creation commit
    today (no plural /Commits endpoint exists yet), so the timeline scrubber's
    tick marks are limited when running against a real backend.

@bhudevbhanpuriya

Copy link
Copy Markdown
Author

Update — follow-up commit (0b06278)

Refines the As-Of chart behavior and fixes an existence-state bug found while
testing against a live istSOS4 backend (VERSIONING=1). No new files; touches
5 existing ones. Verified with tsc --noEmit (clean) and against the running
backend API.

Chart in snapshot mode

  • The observations chart now reacts to asOfDate changes (scrubber, calendar,
    and the existence-dialog "jump to nearest data") instead of staying stuck on a
    stale window from a previous snapshot. The refetch is debounced on the trailing
    edge so continuous scrubbing fires a single request.
  • Default window is strictly [asOfDate − 7d, asOfDate]; the persisted range and
    observation cache are cleared on any snapshot/live context switch.
  • Refetches the whole current selection (all things / observed properties) and
    preserves an active comparison across snapshot changes, not just the primary
    datastream.

Empty-window feedback

  • When the snapshot window has no data, the chart shows an informational warning
    describing where the data actually is (before/after the range) — without ever
    mutating the as-of state. This is expected with the demo data, where measurements
    (phenomenonTime ≈ 2020) were committed to the DB in 2026, so a recent snapshot's
    7-day window is legitimately empty until you pan the picker back.

Chart rendering

  • Pins the x-axis to the queried window in snapshot mode, so sparse/empty data still
    renders the full range and the snapshot marker line can't stretch the axis.
  • Draws the snapshot marker line once (primary series) instead of once per series.

Existence-state fix (asOfAdapter.ts)

  • "Not-yet-created vs deleted" is now derived from the Thing's commit history
    (transaction time — the axis $as_of filters on)
    instead of phenomenonTime,
    which can be years off from when the row was written. Previously a snapshot that
    predated a Thing's creation was mislabeled "deleted" with a wrong jump target.
  • fetchCommits now reads the plural /Things(id)/Commits collection for full
    history (falling back to the singular /Commit).

@bhudevbhanpuriya

Copy link
Copy Markdown
Author

rebased on latest main, no conflicts, and moved all UI strings to i18n

Introduces a full As-Of (time-travel) feature for the map/datastream
UI, letting users view the SensorThings graph as it existed at a past
point in time instead of live.

State & data layer:
- AsOfContext/AsOfProvider (context/AsOfContext.tsx): global
  asOfDate/isSnapshot state, exposed via useAsOf().
- asOfAdapter.ts: single AsOfAdapter interface (resolveThing,
  fetchCommits) with two implementations selected by
  NEXT_PUBLIC_VERSIONING_ENABLED:
    - mockAdapter: resolves state from local-dummy-things.ts, for
      demo/dev use with no backend versioning support.
    - apiAdapter: calls the real backend via
      Things(id)?$as_of=<date> and Things(id)/Commit.
- useAsOfThing / useAsOfCommits: hooks wrapping the adapter, exposing
  the resolved snapshot Thing, existence state
  (exists/not-yet-created/deleted), and commit history for a Thing.

UI components (features/as-of/components/):
- AsOfNavbarButton + AsOfDropdown: navbar pill (Live / snapshot chip)
  and a calendar+time picker with quick-range presets, with a
  future-date guard.
- AsOfBanner: page-wide banner shown while in snapshot mode.
- TimelineScrubber: bottom-fixed slider across [earliest commit, now]
  with clickable commit tick marks.
- AsOfExistenceDialog: dismissible floating alert with a "jump to
  nearest valid date" action when asOfDate falls outside a Thing's
  known lifetime.

Wiring into the app:
- layout.tsx: wraps the app in AsOfProvider, renders AsOfBanner.
- Navbar.tsx: adds AsOfNavbarButton, reworks the navbar into a
  3-column grid to fit it.
- Home.tsx: resolves the selected Thing's snapshot via useAsOfThing
  and feeds the resolved snapshotThing (not the live thing) into
  DatastreamTable; renders TimelineScrubber and
  AsOfExistenceDialog; re-arms the existence dialog only on a
  genuine transition into an invalid state, not on every scrubber
  tick.
- DatastreamTable.tsx: read-only banner and hides Edit/Delete/New/
  Import actions while isSnapshot is true.
- LeafletMap.tsx / leafletDraw.ts / leafletCluster.ts: snapshot
  markers get an amber date-pill divIcon (createThingMarkerDivIcon);
  MapContextMenu hides "New"/"Import from file" during snapshot mode
  so the map stays read-only like the table.
- ChartModal.tsx / ObservationGraph.tsx / observationGraphOptions.ts:
  clamps the date-range picker to asOfDate, draws a dashed snapshot
  marker line on the chart.
- observations.ts: appends $as_of to the observations request when
  in snapshot mode.
- useChartState.ts: namespaces the observation cache by asOfDate so
  live and snapshot results never collide.
- useDataSourcesSync.ts: injects the local dummy stations onto the
  map only when NEXT_PUBLIC_VERSIONING_ENABLED is false (mock mode).
Follow-up to the As-Of time-travel UI: makes the observations chart
react correctly to snapshot changes and corrects a wrong existence
state when a snapshot predates a Thing's creation.

Chart in snapshot mode (useChartState.ts):
- Re-anchor the chart time range and refetch when asOfDate changes
  (scrubber, calendar, or "jump to nearest data"), instead of leaving
  a stale window from a previous snapshot. Debounced on the trailing
  edge so continuous scrubbing fires a single request.
- Default window is strictly [asOfDate-7d, asOfDate]; clears the
  persisted range and cache on any snapshot/live context switch.
- Refetch the whole current selection (all things / observed
  properties) and preserve an active comparison across snapshot
  changes, not just the primary datastream.

Empty-window feedback (ChartModal.tsx):
- Show an informational warning when the snapshot window has no data,
  describing where the data actually is (before/after the range),
  without ever mutating the as-of state.

Chart rendering (ObservationGraph.tsx, observationGraphOptions.ts):
- Pin the x-axis to the queried window in snapshot mode so sparse or
  empty data still renders the full range and the snapshot markLine
  can't stretch the axis.
- Draw the snapshot markLine once (on the primary series) instead of
  once per series.

Existence-state fix (asOfAdapter.ts):
- Determine not-yet-created vs deleted from the Thing's commit history
  (transaction time, the axis $as_of filters on) instead of
  phenomenonTime, which can be years off from when the row was
  written. Fixes the dialog showing the wrong direction / jump target
  when a snapshot predates a Thing's creation.
- fetchCommits now reads the plural /Things(id)/Commits collection for
  full history (falling back to the singular /Commit).
Restore data-sources.json authorizationEnabled to true and revert the
tsconfig.tsbuildinfo build artifact; both were local testing conveniences
and are not part of the As-Of feature.
All user-facing copy in the As-Of components was hardcoded English while
the rest of the app goes through react-i18next. Move every label, button,
message, tooltip and aria-label into a new as_of namespace and add the
matching en/it translations.

Calendar month and weekday names are resolved through i18n rather than
dayjs locale data so the picker stays translated regardless of the
active dayjs locale. Reuses general.apply for the Apply button.

Also drops an unused asOfDate binding in AsOfExistenceDialog.

Verified: tsc --noEmit clean; en/it key sets identical; every t() key
used in the components resolves; no new eslint problems.
@bhudevbhanpuriya

Copy link
Copy Markdown
Author

Update - two new commits since this was last looked at.

1. Removed my local testing changes.
While developing I had turned off authorization in data-sources.json and
committed a generated build file (tsconfig.tsbuildinfo). Both are now reverted.
They were never part of the feature.

2. Added translations.
All the text in the As-Of UI was written directly in English, but the rest of the
app reads its text from the translation files so it can switch between English
and Italian. I moved every piece of visible text — buttons, labels, warnings,
hover text — into en/translation.json and it/translation.json, the same way
#20 did for the observation graph. Month and weekday names in the calendar are
translated as well.

What I checked: the TypeScript build passes, the English and Italian files have
exactly the same list of keys, and every piece of text the code asks for exists
in both files. I checked this by reading the code, not by clicking through the
app, so a quick look at the Italian version would be worth it.

There are also 4 warnings in these files that the linter flags. They were there
before these commits — happy to fix them here or leave them for later, your call.

No conflicts with GSoC_2026-TimeTravel. One question: that branch is 8 commits
behind main - would you rather this PR pointed at main?

Next I'm starting the $from_to UI, which reuses these same components, so
feedback on the shared parts here would help before I build on top of them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant