Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1329399
Feat/python glm parser (#25)
itsMando Aug 6, 2026
11c0042
new search field for the model data view
itsMando Aug 6, 2026
d2205ec
updated version number; updated README on instructions for running in…
itsMando Aug 6, 2026
caa4e7f
fix(docker): keep entrypoint script LF so the frontend container starts
itsMando Aug 12, 2026
3780f58
new distribution area highlighting program and fix for building GLIMP…
itsMando Aug 12, 2026
5eff491
improved dark mode response across all components
itsMando Aug 12, 2026
661715e
Merge branch 'master' into develop
itsMando Aug 12, 2026
6c22c94
new distributated area agents view and agents overlay on visualizatio…
itsMando Aug 13, 2026
2bb9fe5
removed parser json outputs
itsMando Aug 18, 2026
16b318d
improved color themeing and constrast (#28)
itsMando Aug 19, 2026
9cf5c5d
fixed an issue with the legend panel not updating colors when switchi…
itsMando Aug 19, 2026
a6ff809
merged master and updated some depricated fields
itsMando Aug 25, 2026
641148e
removed many verbose comment blocks
itsMando Aug 26, 2026
7bcbdc2
removed verbose comments
itsMando Aug 27, 2026
ce233a3
fixed an issue where viewing association of cim objects would show a …
itsMando Aug 28, 2026
515476b
removed verbose comments
itsMando Aug 28, 2026
02b0638
removed verbose comments
itsMando Aug 28, 2026
33a9bae
set default appearance to dark mode
itsMando Aug 28, 2026
86945c5
new dev container configuration
itsMando Aug 29, 2026
aa0972e
dev container config for codespace
itsMando Sep 2, 2026
b880aa4
removed the agents overlay that is intended for gridappsd features only
itsMando Sep 2, 2026
ab08074
improved error handling and resiliance for the entire application
itsMando Sep 5, 2026
0388a01
removed hosted conditional code and verbose comments
itsMando Sep 12, 2026
e736772
Add GitHub Actions workflow for Docker images and update actions (#30)
craigpnnl Sep 14, 2026
8e178e0
updated readme
itsMando Sep 15, 2026
f57ad35
Merge branch 'master' into develop
itsMando Sep 15, 2026
9907585
improved readability and code consistancy
itsMando Sep 15, 2026
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
136 changes: 136 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
on:
push:
workflow_dispatch:
inputs:
dry_run:
description: "Build only, skip docker push"
type: boolean
default: false

env:
DOCKER_PROJECT: gridappsd

jobs:
push:
runs-on: ubuntu-latest
name: Build and push the docker container
strategy:
matrix:
include:
- image_name: glimpse-backend
dockerfile: Dockerfile.backend
readme: docker/README-backend.md
- image_name: glimpse-frontend
dockerfile: Dockerfile.frontend
readme: docker/README-frontend.md
env:
DOCKER_IMAGE_NAME: ${{ matrix.image_name }}
SKIP_BUILD: "false"
steps:
- uses: actions/checkout@v7

- name: Checking environment
run: |
if [ "x${{ env.DOCKER_IMAGE_NAME }}" == "x" ]; then
echo "Error: missing DOCKER_IMAGE_NAME"
exit 1
fi

OWNER=`echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]'`
PROJECT=`echo "${{ env.DOCKER_PROJECT }}" | tr '[:upper:]' '[:lower:]'`
if [ "$OWNER" != "$PROJECT" ]; then
echo "Skipping: repository owner '$OWNER' does not match DOCKER_PROJECT '$PROJECT'"
echo "SKIP_BUILD=true" >> $GITHUB_ENV
else
echo "SKIP_BUILD=false" >> $GITHUB_ENV
fi

- name: Log in to docker
if: env.SKIP_BUILD != 'true'
run: |
if [ -n "${{ secrets.DOCKER_USERNAME }}" -a -n "${{ secrets.DOCKER_TOKEN }}" ]; then

echo " "
echo "Connecting to docker"
echo "${{ secrets.DOCKER_TOKEN }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
status=$?
if [ $status -ne 0 ]; then
echo "Error: status $status"
exit 1
fi
fi

- name: Build the image
if: env.DOCKER_IMAGE_NAME != null && env.SKIP_BUILD != 'true'
run: |
TAG="${GITHUB_REF#refs/heads/}"
TAG="${TAG#refs/tags/}"
TAG="${TAG//\//_}"
ORG=`echo "${{ env.DOCKER_PROJECT }}" | tr '[:upper:]' '[:lower:]'`
ORG="${ORG:-gridappsd}"
ORG="${ORG:+${ORG}/}"
IMAGE="${ORG}${{ env.DOCKER_IMAGE_NAME }}"
TIMESTAMP=`date +'%y%m%d%H'`
GITHASH=`git log -1 --pretty=format:"%h"`
BUILD_VERSION="${TIMESTAMP}_${GITHASH}${BRANCH:+:$TAG}"
echo "BUILD_VERSION $BUILD_VERSION"
echo "TAG ${IMAGE}:${TIMESTAMP}_${GITHASH}"
docker build --build-arg VERSION="${TAG}" --build-arg TIMESTAMP="${BUILD_VERSION}" -f ${{ matrix.dockerfile }} -t ${IMAGE}:${TIMESTAMP}_${GITHASH} .
status=$?
if [ $status -ne 0 ]; then
echo "Error: status $status"
exit 1
fi


- name: Push the image
if: env.DOCKER_IMAGE_NAME != null && env.SKIP_BUILD != 'true' && github.event.inputs.dry_run != 'true'
run: |
TAG="${GITHUB_REF#refs/heads/}"
TAG="${TAG#refs/tags/}"
TAG="${TAG//\//_}"
ORG=`echo "${{ env.DOCKER_PROJECT }}" | tr '[:upper:]' '[:lower:]'`
ORG="${ORG:-gridappsd}"
ORG="${ORG:+${ORG}/}"
IMAGE="${ORG}${{ env.DOCKER_IMAGE_NAME }}"
if [ -n "${{ secrets.DOCKER_USERNAME }}" -a -n "${{ secrets.DOCKER_TOKEN }}" ]; then

if [ -n "$TAG" -a -n "$ORG" ]; then
# Get the built container name
CONTAINER=`docker images --format "{{.Repository}}:{{.Tag}}" ${IMAGE}`

echo "docker push ${CONTAINER}"
docker push "${CONTAINER}"
status=$?
if [ $status -ne 0 ]; then
echo "Error: status $status"
exit 1
fi

echo "docker tag ${CONTAINER} ${IMAGE}:$TAG"
docker tag ${CONTAINER} ${IMAGE}:$TAG
status=$?
if [ $status -ne 0 ]; then
echo "Error: status $status"
exit 1
fi

echo "docker push ${IMAGE}:$TAG"
docker push ${IMAGE}:$TAG
status=$?
if [ $status -ne 0 ]; then
echo "Error: status $status"
exit 1
fi
fi

fi

- name: Update Docker Hub overview
if: env.DOCKER_IMAGE_NAME != null && env.SKIP_BUILD != 'true' && github.event.inputs.dry_run != 'true'
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
repository: ${{ env.DOCKER_PROJECT }}/${{ env.DOCKER_IMAGE_NAME }}
readme-filepath: ${{ matrix.readme }}
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ lerna-debug.log*
**/node_modules
**/gridappsd-viz
**/__pycache__
**/glm
**/.claude
**/.venv
**/.env
Expand All @@ -22,13 +21,12 @@ lerna-debug.log*
**/env/
**/CIM-Builder
*.local
Dockerfile
.dockerignore

CLAUDE.md

# Editor directories and files
.vscode/*
.zed/*
!.vscode/extensions.json
.idea
.DS_Store
Expand Down
8 changes: 1 addition & 7 deletions Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@
# ---- builder: install Python deps into /usr/local --------------------------
FROM python:3.12-slim AS builder

# uv for fast installs; git to clone CIM-Builder.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install server deps (no pyinstaller). --prerelease=allow: cim-graph is 0.4.3a10.
COPY local-server/requirements.txt ./requirements.txt
RUN uv pip install --system -r requirements.txt

Expand All @@ -22,8 +17,7 @@ ENV PYTHONUNBUFFERED=1 \
FLASK_HOST=0.0.0.0 \
FLASK_PORT=5052

# Copy everything uv installed (site-packages + console scripts). git and uv
# stay behind in the builder stage and never reach the final image.
# Copy everything uv installed (site-packages + console scripts); uv stays behind.
COPY --from=builder /usr/local /usr/local

WORKDIR /app
Expand Down
14 changes: 4 additions & 10 deletions Dockerfile.frontend
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,14 @@ RUN npm run build
# ---- serve stage -----------------------------------------------------------
FROM nginx:alpine

# openssl is only needed so the TLS entrypoint can generate a self-signed
# certificate when none is mounted (see docker/45-glimpse-tls.sh).
RUN apk add --no-cache openssl

COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

# Runtime env injection (writes env.js + patches CSP from $API_URL on startup)
# and TLS provisioning. nginx:alpine runs /docker-entrypoint.d/*.sh in order.
# Runtime env injection: writes env.js + patches CSP from $API_URL on startup.
COPY docker/40-glimpse-env.sh /docker-entrypoint.d/40-glimpse-env.sh
COPY docker/45-glimpse-tls.sh /docker-entrypoint.d/45-glimpse-tls.sh
# Strip CRs in case of a Windows checkout — a "#!/bin/sh\r" shebang fails at
# startup with a confusing "not found".
RUN sed -i 's/\r$//' /docker-entrypoint.d/40-glimpse-env.sh /docker-entrypoint.d/45-glimpse-tls.sh \
&& chmod +x /docker-entrypoint.d/40-glimpse-env.sh /docker-entrypoint.d/45-glimpse-tls.sh
RUN sed -i 's/\r$//' /docker-entrypoint.d/40-glimpse-env.sh \
&& chmod +x /docker-entrypoint.d/40-glimpse-env.sh

EXPOSE 80 443
EXPOSE 80
36 changes: 0 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ authenticate against. The backend is not reachable from outside the Codespace.
> unavailable in a Codespace — there is no broker to connect to. GLIMPSE detects this at startup and
> disables those panels; file upload, visualization, editing, and export all work normally.


### Option 4: Build From Source

#### Quick Overview
Expand Down Expand Up @@ -281,41 +280,6 @@ The finished installer is written to the `release/` directory. The installed app
> [!TIP]
> If `pyinstaller` is not on your PATH, activate the Python environment you created for `local-server/` first (or, with UV, run `uv run pyinstaller server.spec --noconfirm` inside `local-server/`).

### Deployment & Security Configuration

> [!NOTE]
> GLIMPSE runs a _single-session_ server: one loaded model, shared by every
> connected client. It is built for one user at a time, whether that is the
> desktop app or a Docker container on a machine you control.

The desktop app runs the backend bound to `127.0.0.1` (loopback only), so the defaults below are safe as-is. **A networked deployment is different**: the Docker backend binds to `0.0.0.0`, which makes it reachable by any client that can route to the port. Because the backend has no per-user login, treat the following environment variables as required hardening before exposing it beyond localhost.

| Variable | Applies to | Default | Purpose |
| :------------------------------------------------------------------------------- | :----------------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GLIMPSE_API_TOKEN` | backend + frontend | _(empty → auth **off**)_ | Shared bearer token. When set, **every** HTTP request and WebSocket connection must present it or is rejected (`401` / refused handshake). Compose passes the same value to the backend (`GLIMPSE_API_TOKEN`) and the frontend (`API_TOKEN`). |
| `CORS_ORIGINS` | backend | local dev ports | Comma-separated list of browser origins allowed to call the API (e.g. `https://glimpse.example.org`). `*` allows any origin but **disables credentialed CORS**. Pin this to your frontend's real origin in production. |
| `GLIMPSE_EXPORT_DIR` | backend | system temp `/glimpse_exports` | Directory that CIM export writes are confined to. Client-supplied export paths are resolved inside this directory; absolute paths and `..` traversal are rejected. |
| `GLIMPSE_ALLOW_ANY_EXPORT_PATH` | backend | `0` | Set to `1` only for a **desktop** build where the user intentionally picks any save location. Disables the export-path confinement above — do not enable on a shared/networked server. |
| `MAX_UPLOAD_MB` | backend | `50` | Maximum request body size (MB) for uploads, to bound memory use. Requests over the limit get `413`. |
| `GLIMPSE_MODELS_DIR` | backend | auto-detected | Directory holding the bundled example models offered in the "Example Models" tab. By default the backend looks for a `models/` folder next to the server (PyInstaller bundle / Docker bind mount) and then the repo's top-level `models/` folder. Missing files are simply not offered. |
| `EXPOSE_TRACEBACKS` | backend | `0` | When `1`, includes Python tracebacks in error responses (useful for local debugging). Leave off in production so internal details aren't leaked to clients. |
| `GRIDAPPSD_ADDRESS` / `GRIDAPPSD_PORT` / `GRIDAPPSD_USER` / `GRIDAPPSD_PASSWORD` | backend | `localhost` / `61613` / `system` / `manager` | GridAPPS-D broker connection. The defaults are GridAPPS-D's own defaults — **change the credentials** for any real broker and source them from your secret store, not the compose file. |
| `CIMG_URL` | backend | derived from `GRIDAPPSD_ADDRESS` | Blazegraph SPARQL endpoint used for CIM model loads (`http://<GRIDAPPSD_ADDRESS host>:8889/bigdata/namespace/kb/sparql` by default, matching a standard GridAPPS-D deployment). Set explicitly when Blazegraph runs on a different host/port. The other `CIMG_*` cimgraph settings can be overridden the same way. |
| `GLIMPSE_SPARQL_TIMEOUT` | backend | `120` | Per-query timeout (seconds) for Blazegraph SPARQL queries during CIM model loads, so one stalled query can't hang a load forever. Raise it if a very slow Blazegraph instance times out on large models. |
| `GLIMPSE_SPARQL_MAX_CONCURRENT` | backend | `4` | Maximum in-flight SPARQL queries during a CIM model load. Caps the pressure on the Blazegraph JVM when loading large models (e.g. IEEE 9500); raise it on a beefy Blazegraph host for faster loads. |

#### Enabling authentication

Generate a random secret and set it before starting the stack — both containers pick it up:

```bash
export GLIMPSE_API_TOKEN="$(openssl rand -hex 32)"
docker compose up --build
```

> [!IMPORTANT]
> This token is a **coarse gate**, not per-user authentication. It is embedded in the frontend bundle (served in `env.js` and sent on every request), so anyone who can load the UI can read it. Its job is to keep arbitrary network clients that _don't_ have the frontend from reaching the `0.0.0.0`-bound backend. If you need real per-user authorization, put GLIMPSE behind an authenticating reverse proxy or add session/OAuth login on top of this gate. For anything sensitive, also terminate TLS at a proxy so the token isn't sent in cleartext.

## Supported Input Files

### JSON Formats
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ services:
context: .
dockerfile: Dockerfile.backend
environment:
FLASK_HOST: 0.0.0.0
FLASK_PORT: "5052"
CORS_ORIGINS: "${CORS_ORIGINS:-}"
GLIMPSE_API_TOKEN: "${GLIMPSE_API_TOKEN:-}"
GRIDAPPSD_ADDRESS: "${GRIDAPPSD_ADDRESS:-host.docker.internal}"
Expand Down
30 changes: 30 additions & 0 deletions docker/README-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# GLIMPSE Backend

Flask + SocketIO backend for [GLIMPSE](https://github.com/pnnl/GLIMPSE), a
graph-based desktop application to visualize and update GridLAB-D power grid
models.

This image serves the GLIMPSE API and connects to a GridAPPS-D broker and a
Blazegraph SPARQL endpoint for CIM model loads. Pair it with the
`gridappsd/glimpse-frontend` image using the project's
[docker-compose.yml](https://github.com/pnnl/GLIMPSE/blob/main/docker-compose.yml).

## Quick start

```bash
git clone http://github.com/pnnl/GLIMPSE
cd GLIMPSE
docker compose up --build
```

## Configuration

Key environment variables:

- `FLASK_HOST` / `FLASK_PORT` - bind address and port (default `0.0.0.0:5052`)
- `CORS_ORIGINS` - comma-separated allowed browser origins
- `GLIMPSE_API_TOKEN` - shared bearer token for API/socket auth
- `GRIDAPPSD_ADDRESS`, `GRIDAPPSD_PORT`, `GRIDAPPSD_USER`, `GRIDAPPSD_PASSWORD` - GridAPPS-D broker connection
- `CIMG_URL` - Blazegraph SPARQL endpoint (derived from `GRIDAPPSD_ADDRESS` if unset)

See the [README](https://github.com/pnnl/GLIMPSE#readme) for full details.
28 changes: 28 additions & 0 deletions docker/README-frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# GLIMPSE Frontend

React frontend (served by nginx) for [GLIMPSE](https://github.com/pnnl/GLIMPSE),
a graph-based desktop application to visualize and update GridLAB-D power grid
models.

This image serves the GLIMPSE UI and talks to the `gridappsd/glimpse-backend`
image over its API/socket endpoint. Pair the two using the project's
[docker-compose.yml](https://github.com/pnnl/GLIMPSE/blob/main/docker-compose.yml).

## Quick start

```bash
git clone http://github.com/pnnl/GLIMPSE
cd GLIMPSE
docker compose up --build
```

Then open `http://localhost:5173`.

## Configuration

Key environment variables:

- `API_URL` - URL the browser uses to reach the backend (default `http://127.0.0.1:5052`)
- `API_TOKEN` - must match the backend's `GLIMPSE_API_TOKEN` (baked into `env.js` at container start)

See the [README](https://github.com/pnnl/GLIMPSE#readme) for full details.
24 changes: 7 additions & 17 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ let quitting = false;
const MAX_SERVER_RESTARTS = 1;
let restartsUsed = 0;

const notifyRenderer = (channel) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel);
}
};

app.commandLine.appendSwitch("enable-unsafe-swiftshader");

const isWSL =
Expand Down Expand Up @@ -82,18 +76,15 @@ const startServer = () => {
if (restartsUsed < MAX_SERVER_RESTARTS) {
restartsUsed += 1;
console.warn(`[server] exited (code ${code}, signal ${signal}) — restarting.`);
notifyRenderer("backend-restarting");

startServer();
waitForServer()
.then(() => notifyRenderer("backend-restarted"))
.catch((err) => {
dialog.showErrorBox(
"GLIMPSE backend stopped",
`The local server exited and could not be restarted.\n\n${err.message}`,
);
app.quit();
});
waitForServer().catch((err) => {
dialog.showErrorBox(
"GLIMPSE backend stopped",
`The local server exited and could not be restarted.\n\n${err.message}`,
);
app.quit();
});
return;
}

Expand Down Expand Up @@ -248,7 +239,6 @@ const createWindow = () => {
sandbox: true,
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
},
});

Expand Down
7 changes: 1 addition & 6 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,8 @@ export default defineConfig([
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
Expand Down
Loading