Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

- **Security — opened notebooks:** an opened `.ipynb` file can no longer change the look of DataLab-Web, cover it or contact a remote server. Its markdown cells and its HTML and SVG outputs are now displayed in their own isolated block: their styles no longer apply to the rest of the interface, and content positioned over the whole window stays inside the block, so it cannot hide commands or imitate a DataLab-Web dialog. Form fields are removed, and nothing is loaded from the network (remote images, style sheets, fonts or backgrounds), which prevents a notebook from tracking its opening. Images embedded in the notebook and the tables and styling of Sigima results are displayed as before.
- **Unsaved changes:** several edits did not mark the workspace as modified, so closing or reloading the page discarded them without warning: editing signal or image values in the _Data_ array editor, duplicating objects, moving an object up or down, pasting or importing a region of interest, changing the resampling method of an image, plotting analysis results, committing an interactive fit, importing nodes from an HDF5 file and running a plugin menu action. They now mark the workspace as modified, like the other edits.
- **On-disk storage — failed writes:** when the browser refused to write an object to disk (storage quota exceeded, write error), the object lost its data: it had already been removed from memory. The object now stays in memory with its data, and the error is reported. An interrupted write no longer replaces the previous on-disk copy either.
- **On-disk storage — several tabs:** with DataLab-Web open in two tabs in on-disk storage mode, switching one tab back to memory storage or replacing its workspace deleted the data stored on disk by the other tab, whose objects could then no longer be displayed or processed. Each tab now keeps its data in its own storage area; the areas of closed tabs are reclaimed when the on-disk mode is next enabled.
- **HDF5 import — complex and integer data:** single-precision complex signals (`complex64`) lost their imaginary part, and unsigned 32-bit images were clipped to 2 147 483 647. Complex data is now imported as complex objects and integer data keeps its values exactly (images with large values become 64-bit floating-point images); a warning now reports integers beyond 2⁵³, which cannot be represented exactly. Previews show the real part of complex data.
- **HDF5 browser — broken links:** files containing a link to a missing object or to a missing file (e.g. synchrotron master files without their detector files) could not be opened at all. Broken links are now skipped and the rest of the file can be browsed and imported.
- **HDF5 browser — link cycles:** files containing a group linked into itself or into one of its subgroups were browsed down to a hundred phantom levels, or took too long to open. Such links back to a parent group are now skipped; other aliases are still shown at each of their paths.
- **HDF5 browser — large files:** opening an HDF5 file no longer reads every dataset entirely, including those that cannot be imported (e.g. 3D cubes): datasets are described from a small sample, and their data is read only when previewed or imported. Large files open faster and with much less memory.
- **Embedding — message origins:** the JavaScript SDK accepted responses and events from any document loaded in the DataLab-Web iframe, and DataLab-Web sent its events to host windows whatever document they showed. The SDK now only accepts messages from the configured `targetOrigin`, and DataLab-Web sends its responses and events to the origin from which each host window made its requests.

### Changed

- **Data locality:** the README now states precisely what stays local: processing never requires sending data anywhere, and the network services that can be enabled (AI assistant provider, capsule registry) are named.

## [0.10.0] - 2026-10-20

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ DataLab Web embeds the [Sigima](https://github.com/DataLab-Platform/Sigima) comp

**👉 <https://datalab-platform.com/web/>**

The latest release is deployed automatically to GitHub Pages. Open the link in any modern browser (Chrome, Edge, Firefox, Safari) and the full DataLab application — Sigima, NumPy, SciPy, scikit-image, h5py — runs locally inside the browser tab. No server, no account, no upload: your data never leaves your machine.
The latest release is deployed automatically to GitHub Pages. Open the link in any modern browser (Chrome, Edge, Firefox, Safari) and the full DataLab application — Sigima, NumPy, SciPy, scikit-image, h5py — runs locally inside the browser tab. No server, no account, no upload: processing your data never requires sending it anywhere. Network services are used only when you enable them: the AI assistant sends your requests (and the context you share) to the provider you configure, and capsules and their reviews are sent to a capsule registry only when you configure one and publish them.

> First load downloads Pyodide and installs Sigima via `micropip` (~30–60 s). Subsequent loads are cached by the browser. On Microsoft Edge the first load may be much slower — see [doc/troubleshooting.md](doc/troubleshooting.md) for the one-line fix.

Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const data = await client.getSignalXY(ids[0]);

The DataLab-Web iframe **must** be loaded with `?allowedOrigins=<host-origin>` in its URL (or `?allowedOrigins=*` for development) so the bridge accepts incoming messages.

`targetOrigin` must be the origin of the iframe document: the client only accepts responses and events coming from the iframe window **and** from that origin, so that another document loaded in the iframe (after a navigation) cannot answer in its place. `targetOrigin: "*"` accepts any origin and is intended for local development only. Likewise, the bridge sends its responses and events to each host window with the origin of its requests.

## Wire-protocol versioning

`client.ready()` negotiates a wire-protocol version with the iframe (semver `MAJOR.MINOR`). It rejects if the iframe reports a MAJOR incompatible with the SDK (`SUPPORTED_PROTOCOL_MAJOR`).
Expand Down
9 changes: 7 additions & 2 deletions packages/sdk/src/DataLabWebClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

export interface DataLabWebClientOptions {
/** Origin of the DataLab-Web iframe. Required so we don't leak
* request payloads to the wrong document. Use the iframe's
* ``src`` origin (e.g. ``"https://datalab.example.com"``). */
* request payloads to the wrong document, nor accept responses and
* events from another document loaded in the iframe. Use the iframe's
* ``src`` origin (e.g. ``"https://datalab.example.com"``); ``"*"``
* accepts any origin and is intended for local development only. */
targetOrigin: string;
/** Default request timeout in ms. ``0`` disables. Defaults to 30 s. */
defaultTimeoutMs?: number;
Expand Down Expand Up @@ -492,6 +494,9 @@ export class DataLabWebClient {

private handleMessage = (event: MessageEvent) => {
if (event.source !== this.iframe.contentWindow) return;
// The iframe window keeps its identity when it navigates: also check that
// the message comes from the expected origin (any origin with "*")
if (this.targetOrigin !== "*" && event.origin !== this.targetOrigin) return;
const data = event.data as RpcResponse | RpcEvent | null;
if (!data || typeof data !== "object") return;
if (data.type === "response") {
Expand Down
16 changes: 9 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3677,11 +3677,12 @@ export default function App() {
Array.from(supported),
null,
);
if (result.uint32_clipped) {
if (result.inexact_values) {
await notify({
kind: "warning",
message:
"Some uint32 image data was clipped to int32 range during import.",
message: t(
"Some integer values could not be represented exactly and were rounded during import.",
),
});
}
setWorkspaceVersion((v) => v + 1);
Expand Down Expand Up @@ -3915,13 +3916,14 @@ export default function App() {
);

const handleH5BrowserImport = useCallback(
async (oids: string[], uint32Clipped: boolean) => {
async (oids: string[], inexactValues: boolean) => {
setH5BrowserFiles(null);
if (uint32Clipped) {
if (inexactValues) {
await notify({
kind: "warning",
message:
"Some uint32 image data was clipped to int32 range during import.",
message: t(
"Some integer values could not be represented exactly and were rounded during import.",
),
});
}
await refreshImportedObjects(oids);
Expand Down
8 changes: 4 additions & 4 deletions src/components/H5BrowserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface Props {
initial?: H5BrowserFile[];
/** Called with the list of object ids created in the model when the
* user clicks OK. */
onImport: (oids: string[], uint32Clipped: boolean) => void;
onImport: (oids: string[], inexactValues: boolean) => void;
onCancel: () => void;
}

Expand Down Expand Up @@ -362,7 +362,7 @@ export function H5BrowserDialog({ initial, onImport, onCancel }: Props) {
setBusy(true);
setError(null);
const allOids: string[] = [];
let uintClipped = false;
let inexactValues = false;
// Flatten (file, node) pairs so the progress bar reflects per-node
// granularity. Each node is imported individually to keep the bar
// responsive and cancellation reactive between nodes.
Expand All @@ -381,7 +381,7 @@ export function H5BrowserDialog({ initial, onImport, onCancel }: Props) {
setLabel(`${i + 1} / ${tasks.length} — ${nodeId}`);
const result = await runtime.importH5BrowserNodes(fileId, [nodeId]);
allOids.push(...result.oids);
if (result.uint32_clipped) uintClipped = true;
if (result.inexact_values) inexactValues = true;
},
});
if (cancelled && allOids.length === 0) {
Expand All @@ -394,7 +394,7 @@ export function H5BrowserDialog({ initial, onImport, onCancel }: Props) {
await Promise.all(
ids.map((fid) => runtime.closeH5Browser(fid).catch(() => {})),
);
onImport(allOids, uintClipped);
onImport(allOids, inexactValues);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setBusy(false);
Expand Down
1 change: 1 addition & 0 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@
"Sobel filter (vertical)": "Filtre de Sobel (vertical)",
"Software": "Logiciel",
"Solid": "Continu",
"Some integer values could not be represented exactly and were rounded during import.": "Certaines valeurs entières ne pouvaient pas être représentées exactement et ont été arrondies lors de l'import.",
"Some selected objects have regions of interest. Do you want to delete them as well?": "Certains objets sélectionnés ont des régions d'intérêt. Souhaitez-vous les supprimer également ?",
"Source": "Source",
"Source {index}": "Source {index}",
Expand Down
15 changes: 11 additions & 4 deletions src/runtime/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3143,13 +3143,16 @@ def detach_object_array(oid: str) -> bytes:
return payload


def attach_object_array(oid: str, payload: bytes) -> bool:
def attach_object_array(oid: str, payload: bytes, on_disk: bool = True) -> bool:
"""Restore *oid*'s heavy array from ``.npy`` *payload* (page-in).

Args:
oid: object identifier in :data:`_MODEL`.
payload: the ``.npy`` bytes previously returned by
:func:`detach_object_array` (read back from OPFS by JS).
on_disk: whether the on-disk copy of the object is *payload* (page-in).
False when *payload* could not be persisted: the object then has no
current on-disk copy, and is serialized again before being released.

Returns:
``True`` when the array was restored, ``False`` when *oid* was not
Expand All @@ -3174,7 +3177,10 @@ def attach_object_array(oid: str, payload: bytes) -> bool:
arr = np.load(io.BytesIO(data), allow_pickle=False)
setattr(entry.obj, _SPILLED[oid]["attr"], arr)
_SPILLED.pop(oid, None)
_PAGED_IN_REVISIONS[oid] = _DATA_REVISIONS.get(oid, 0)
if on_disk:
_PAGED_IN_REVISIONS[oid] = _DATA_REVISIONS.get(oid, 0)
else:
_PAGED_IN_REVISIONS.pop(oid, None)
return True


Expand Down Expand Up @@ -7474,13 +7480,14 @@ def h5_browser_import(
) -> dict[str, Any]:
"""Import every node in *node_ids* into the live model.

Returns ``{"oids": [...], "uint32_clipped": bool}``.
Returns ``{"oids": [...], "inexact_values": bool}``, *inexact_values*
being True when integer values could not all be represented exactly.
"""
result = _h5br.import_nodes(file_id, node_ids, group_id=group_id)
oids: list[str] = []
for kind, obj in result["objects"]:
oids.append(_MODEL.add_object(kind, obj, group_id=result["group_id"]))
return {"oids": oids, "uint32_clipped": bool(result["uint32_clipped"])}
return {"oids": oids, "inexact_values": bool(result["inexact_values"])}


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading