Skip to content

Integrate to New Backend#209

Draft
manuvanegas wants to merge 9 commits into
openskope:nuxt-3-upgradefrom
manuvanegas:main
Draft

Integrate to New Backend#209
manuvanegas wants to merge 9 commits into
openskope:nuxt-3-upgradefrom
manuvanegas:main

Conversation

@manuvanegas
Copy link
Copy Markdown

  • Implement async submit → poll → refine workflow for dataset time series requests
  • Track per-variable job IDs in the dataset store with a centralized resolveTimeSeries logic
  • Add COG raster tile layer via MapLibre for step-by-step map visualization
  • Double-buffered tile swapping with stepReady event for smooth map transitions from one step to the next
  • Add Skope colorbar with loading state, vmax percent control

manuvanegas and others added 9 commits April 14, 2026 21:00
Replace direct TIMESERIES endpoint calls with a submit/poll/refine workflow for long-running time series extraction and analysis. Added new API constants (TIMESERIES_SUBMIT_ENDPOINT, TIMESERIES_STATUS_ENDPOINT, TIMESERIES_REFINE_ENDPOINT) and implemented submitTimeSeriesRequest, pollTimeSeriesStatus (with timeout and error mapping), and refineTimeSeriesAnalysis in the legacy store actions composable. Updated visualize and analyze pages to use the new flow (submit -> poll -> set jobId -> refine) and added improved error handling and user messages for timeouts, server errors, and missing baseline data. Also added jobId state and setter to the analysis store to persist extraction IDs.
…e logic

Refactor time-series request handling to track per-variable job IDs in the dataset store and centralize resolve logic.

- Add dataset.jobIds state with setJobId and clearJobIds; clear job IDs and time series when saving GeoJSON.
- Introduce resolveTimeSeries in useLegacyStoreActions to attempt refine (reuse) and fall back to submit+poll for new jobs.
- Update analyze and visualize pages to use datasetStore.jobIds[varId] and datasetStore.setJobId, remove duplicated requestJson helpers, and improve time-series loading/error handling and messages.
- Remove jobId from analysis store (moved to dataset store) and delete related setters.

These changes enable per-variable job tracking, better reuse of existing jobs, and consolidated error/state management for time-series requests.
Introduce raster COG tile support and tile endpoint handling for MapLibre maps. Changes include:

- Expose TILES_ENDPOINT constant from store (API_HOST_URL/tiles).
- Rename Map component prop `year` -> `step` and use it to select time slice tiles.
- Build COG tile URLs from dataset/variable and step (including rescale and colormap params) and warn/fallback if variable min/max are missing.
- Add functions to add, remove and update a raster layer (COG) using maplibre raster sources/tiles and respect dataset extents as bounds.
- Add watchers to recreate/update the raster layer when `step` or the variable (cog base URL) changes.
- Ensure metadata extent fill layer uses a centralized FILL_LAYER_ID and add the COG layer beneath it.
- Enable raster display in the dataset visualize page and initialize the selected year from the dataset temporalRangeMin.

Also includes small TODOs in code for colormap/time-resolution flexibility.
Introduce double-buffered raster loading for COG tiles (front/back slots) so step changes are atomic: the new step's tiles load invisibly and swap in all at once on idle, rather than updating tile-by-tile. Add slot management, idle-triggered swap, cancellation, and bounds handling. Expose and emit a new stepReady event (Map and MapLibrePoc) and pass step prop through Map. Wire map stepReady to TimeSeriesPlot.advanceAnimation via refs so the map controls animation progression. Include cleanup of pending swaps on unmount and minor related refactors.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Setting the stage for a possible implementation of flexibility when handling dataset time resolution.
Integrate a SkopeColorbar map control and step-loading UI into the MapLibre component. Key changes:

- Added a new SkopeColorbar utility (app/utils/SkopeColorbar.ts) implementing a MapLibre control that renders a vertical colormap gradient with ticks and optional units, and supports update/show/hide.
- Hooked the colorbar into MapLibre: create/add control on mount, update on min/max/unit changes, and remove on unmount. Added CSS styles for the colorbar.
- Expose legend visibility and variable-derived values (min/max/units/colormap) via computed properties and use dataset variable colormap and stops.
- Introduced a step-loading overlay (progress spinner) while raster tiles swap, and ensured a minimum display duration (STEP_DISPLAY_DURATION_MS = 1000ms) before emitting stepReady. Added timeout/cancel logic to manage step display and map idle swaps.
- Apply color scaling cap (COLOR_MAX_PCT = 0.85) to rescale requests and colorbar vmax to avoid extreme outliers.
- Updated getCogFullUrl to use datasetStore.variable.colormap and the capped max when building COG tile URLs.
- Extended DatasetVariable type in app/stores/dataset.ts to include optional colormap and colormap_stops fields.

These changes add a persistent, interactive legend and improve UX by showing a loading indicator and enforcing a brief display time for each step.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Expose a vmaxPct option on the SkopeColorbar and pass it from the MapLibre PoC so the colorbar can annotate the top tick with a “% of max” note. Tighten dataset typings by expanding DatasetVariable with optional metadata fields and remove some uses of any (variable is no longer cast to any). Also simplify computed accessors in MapLibrePoc for units/min/max and add SVG overflow attribute to the colorbar rendering. These changes improve type safety and allow the UI to indicate the percentage-based vmax annotation.
UX: enlarge MapLibre loading spinner and allow overflow for map container. TimeSeriesPlot: add a full-overlay loading state with progressive messages and timers (start/clear on status change and unmount), hide non-error alerts while loading, and add related CSS. Pages (analyze/visualize): surface user-friendly error notifications via messageStore.error on 504, 5xx and 4xx responses while still updating datasetStore error state. These changes improve feedback for long-running time-series requests and provide clearer error messages to users.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates the UI with a new backend workflow for time series requests (submit → poll → refine), adds per-variable job tracking in the dataset store, and introduces COG tile rendering in MapLibre with a new Skope colorbar and step-based animation/transition behavior.

Changes:

  • Implement centralized async time-series resolution (submit/poll/refine) with per-variable jobIds tracking.
  • Add MapLibre COG raster tile layer support with double-buffered swapping and stepReady events.
  • Add a MapLibre control colorbar with loading-state UX updates in the time series plot.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
app/utils/SkopeColorbar.ts New MapLibre control to render a colorbar/legend for raster tiles.
app/stores/dataset.ts Extends dataset variable metadata typing and adds per-variable jobIds.
app/store/modules/constants.js Adds new backend endpoints for tiles and the submit/status/refine workflow.
app/pages/index.vue Makes landing metadata load client-only (server: false).
app/pages/dataset/[id]/visualize/[variable].vue Switches to step-based map/plot coordination and uses centralized time-series resolution.
app/pages/dataset/[id]/index.vue Makes dataset initialization client-only (server: false).
app/pages/dataset/[id]/analyze/[variable].vue Uses centralized time-series resolution and updates error/loading behavior.
app/composables/useLegacyStoreActions.ts Adds submit/poll/refine/resolve logic and clears job IDs on new geometry.
app/components/dataset/TimeSeriesPlot.vue Renames year→step UI, adds loading overlay and progressive loading messages, and exposes advanceAnimation.
app/components/dataset/MapLibrePoc.client.vue Adds COG raster layer handling, step-ready eventing, loading overlay, and the colorbar control.
app/components/dataset/Map.client.vue Renames prop to step and forwards stepReady.
.gitignore Ignores .DS_Store.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 329 to 331
} finally {
analysisStore.setWaitingForResponse(false);
datasetStore.setTimeSeriesLoaded();
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datasetStore.setTimeSeriesLoaded() is called in finally, so even when the request fails and you set timeout/server/badrequest statuses in catch, the status is immediately overwritten to success. Move setTimeSeriesLoaded() to the success path only (or guard it based on whether an error occurred).

Copilot uses AI. Check for mistakes.
Comment on lines +303 to 309
datasetStore.setTimeSeriesLoading();
try {
const response = await requestJson(TIMESERIES_ENDPOINT, {
method: "POST",
body: JSON.stringify(data),
});
const varId = route.params.variable as string;
const jobId = datasetStore.jobIds?.[varId];
const {newJobId, response} = await legacyActions.resolveTimeSeries(jobId, data);
datasetStore.setJobId(varId, newJobId);
analysisStore.setResponse(response);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retrieveAnalysis() no longer toggles analysisStore.waitingForResponse, but the page still checks analysisStore.waitingForResponse (e.g. to prevent duplicate submits). Either restore analysisStore.setWaitingForResponse(true/false) around the async request, or remove/update the guard to use the dataset loading status instead.

Copilot uses AI. Check for mistakes.
datasetStore.setTimeSeriesBadRequest(detail);
messageStore.error(detail.map((d: any) => d.msg).join(" ") || "Bad request.");
}
} else {
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the non-HTTP-error branch (else when e.response is falsy), the store status is never updated after setTimeSeriesLoading(), so the UI can remain stuck in the loading state on network/client errors. Set an appropriate error/timeout status in the store (or ensure loading is cleared) in this branch as well.

Suggested change
} else {
} else {
datasetStore.setTimeSeriesTimeout();

Copilot uses AI. Check for mistakes.
console.warn(`Variable ${datasetStore.variable?.id} is missing min/max — falling back to rescale 0,100`)
}
const min = datasetStore.variable?.min ?? 0;
const max = datasetStore.variable?.max * COLOR_MAX_PCT ?? 100;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const max = datasetStore.variable?.max * COLOR_MAX_PCT ?? 100; can produce NaN when variable.max is undefined (because undefined * pct is NaN, and ?? does not catch NaN). This will leak into the rescale query param and break tile rendering. Compute the fallback before multiplying (or validate with Number.isFinite).

Suggested change
const max = datasetStore.variable?.max * COLOR_MAX_PCT ?? 100;
const max = (datasetStore.variable?.max ?? 100) * COLOR_MAX_PCT;

Copilot uses AI. Check for mistakes.
Comment on lines 2 to 9
<component
:is="mapComponent"
:year="year"
:step="step"
:display-raster="displayRaster"
:circle-to-polygon-edges="circleToPolygonEdges"
@mapReady="emit('mapReady', $event)"
@stepReady="emit('stepReady')"
/>
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map.client.vue always forwards a step prop to the underlying map component, but LeafletMap.client.vue still declares year (not step). If the app runs with the Leaflet engine (via config/query), the year/step selection will stop working and Vue will treat step as an unknown attribute. Consider mapping step -> year for the Leaflet component (or updating LeafletMap to accept step too).

Copilot uses AI. Check for mistakes.
Comment on lines 64 to +112
@@ -71,9 +73,13 @@ definePageMeta({
const route = useRoute();
const appStore = useAppStore();
const datasetStore = useDatasetStore();
const analysisStore = useAnalysisStore();
const messageStore = useMessagesStore();
const legacyActions = useLegacyStoreActions();

const yearSelected = ref(1500);
const stepSelected = ref(1500);
const mapRef = ref();
const timeSeriesPlotRef = ref();
let stopTimeSeriesWatch: (() => void) | null = null;

const hasValidStudyArea = computed(() => datasetStore.hasGeoJson);
@@ -84,24 +90,12 @@ const analyzeLocation = computed(() => ({
const isLoadingMetadata = computed(() => datasetStore.metadata == null);
const traces = computed(() => [{ ...datasetStore.filteredTimeSeries(), type: "scatter" }]);

function setYear(year: number) {
yearSelected.value = year;
function setStep(step: number) {
stepSelected.value = step;
}

async function requestJson(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
if (!response.ok) {
const responseData = await response
.json()
.catch(() => ({ detail: [{ msg: response.statusText }] }));
const error: any = new Error(`Request failed with status ${response.status}`);
error.response = { status: response.status, data: responseData };
throw error;
}
return response.json();
function onStepReady() {
timeSeriesPlotRef.value?.advanceAnimation();
}

async function updateTimeSeries(data: any) {
@@ -111,10 +105,12 @@ async function updateTimeSeries(data: any) {
}
datasetStore.setTimeSeriesLoading();
try {
const response = await requestJson(TIMESERIES_ENDPOINT, {
method: "POST",
body: JSON.stringify(data),
});
const varId = route.params.variable as string;
console.log("Requesting time series with varId:", varId);
const jobId = datasetStore.jobIds?.[varId];
// console.log("Requesting time series with data:", data, "and jobId:", jobId);
const {newJobId, response} = await legacyActions.resolveTimeSeries(jobId, data);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few unused/leftover items here that will likely trip linting and add noise: analysisStore is imported/instantiated but never used, mapRef is declared but unused, and there is a console.log in the request path. Please remove unused refs/imports and the debug log (and update the existing visualize page unit test expectation for displayRaster, since it is now true).

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +71
const stops = this._colors
.slice()
.reverse()
.map((c, i) => {
const offset = ((i / (this._colors.length - 1)) * 100).toFixed(1);
return `<stop offset="${offset}%" stop-color="${c}"/>`;
})
.join("");

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient stop offset calculation divides by (this._colors.length - 1). If colors is empty or has a single entry, this becomes a divide-by-zero and will render NaN%/Infinity% offsets. Add a guard (e.g., handle 0/1-color cases explicitly) before building stops.

Suggested change
const stops = this._colors
.slice()
.reverse()
.map((c, i) => {
const offset = ((i / (this._colors.length - 1)) * 100).toFixed(1);
return `<stop offset="${offset}%" stop-color="${c}"/>`;
})
.join("");
const stops = (() => {
if (this._colors.length === 0) return "";
if (this._colors.length === 1) {
const color = this._colors[0];
return `<stop offset="0%" stop-color="${color}"/><stop offset="100%" stop-color="${color}"/>`;
}
return this._colors
.slice()
.reverse()
.map((c, i) => {
const offset = ((i / (this._colors.length - 1)) * 100).toFixed(1);
return `<stop offset="${offset}%" stop-color="${c}"/>`;
})
.join("");
})();

Copilot uses AI. Check for mistakes.
Comment on lines +734 to +739
watch(
() => props.step,
(step: number) => {
updateRasterLayer(step);
}
)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This watch(() => props.step, ...) call is missing the terminating );. With the repo's semi: ["error", "always"] lint rule, this will likely fail linting (and it’s inconsistent with other watchers in the file).

Copilot uses AI. Check for mistakes.
addCogRasterLayer(props.step);
}
}
)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This watch(cogBaseUrl, ...) call is also missing the terminating );, which is inconsistent with the rest of the file and can violate the semi lint rule.

Suggested change
)
);

Copilot uses AI. Check for mistakes.
// if no existing job or error status is 404 or 422 (job if not found or invalid), submit a new request
const newJobId = await submitTimeSeriesRequest(requestData);
const response = await pollTimeSeriesStatus(newJobId);
const result = response.result
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon: const result = response.result should end with ; to satisfy the repo’s semi: ["error", "always"] lint rule.

Suggested change
const result = response.result
const result = response.result;

Copilot uses AI. Check for mistakes.
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.

2 participants